From d2dd30fe78fb0d15e92b4b164b76b012fe8c00e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Enrique=20Mu=C3=B1oz=20Mart=C3=ADn?= Date: Wed, 27 Sep 2023 21:38:44 +0200 Subject: [PATCH 1/2] Loans: transfer_debt implementation (#1465) * base implementation * add errors and doc * add tests * add transfer debt event * add benchmarks and weights * fix lints * fix unused warnings * base impl done as loan change * update runtime * simplify amounts check * add tests * add apply benchmark * add permissionless for apply method * fix clippy * add header docs * check transfer_debt with loans of different borrowers * rename input types * minor typo --- pallets/loans/src/benchmarking.rs | 85 ++- pallets/loans/src/entities/changes.rs | 43 ++ pallets/loans/src/entities/input.rs | 58 +++ pallets/loans/src/entities/loans.rs | 33 +- pallets/loans/src/entities/pricing.rs | 54 +- .../loans/src/entities/pricing/internal.rs | 4 +- pallets/loans/src/lib.rs | 240 +++++++-- pallets/loans/src/tests/borrow_loan.rs | 58 +-- pallets/loans/src/tests/close_loan.rs | 24 +- pallets/loans/src/tests/mock.rs | 8 +- pallets/loans/src/tests/mod.rs | 14 +- pallets/loans/src/tests/mutate_loan.rs | 16 +- pallets/loans/src/tests/policy.rs | 4 +- .../loans/src/tests/portfolio_valuation.rs | 28 +- pallets/loans/src/tests/repay_loan.rs | 188 +++---- pallets/loans/src/tests/transfer_debt.rs | 489 ++++++++++++++++++ pallets/loans/src/tests/util.rs | 31 +- pallets/loans/src/tests/write_off_loan.rs | 26 +- pallets/loans/src/types/mod.rs | 35 +- pallets/loans/src/util.rs | 7 +- pallets/loans/src/weights.rs | 10 + runtime/altair/src/weights/pallet_loans.rs | 10 + .../centrifuge/src/weights/pallet_loans.rs | 10 + runtime/common/src/lib.rs | 27 +- .../development/src/weights/pallet_loans.rs | 10 + runtime/integration-tests/src/utils/loans.rs | 11 +- 26 files changed, 1155 insertions(+), 368 deletions(-) create mode 100644 pallets/loans/src/entities/changes.rs create mode 100644 pallets/loans/src/entities/input.rs create mode 100644 pallets/loans/src/tests/transfer_debt.rs diff --git a/pallets/loans/src/benchmarking.rs b/pallets/loans/src/benchmarking.rs index f131546121..e2f81494a5 100644 --- a/pallets/loans/src/benchmarking.rs +++ b/pallets/loans/src/benchmarking.rs @@ -36,17 +36,19 @@ use sp_std::time::Duration; use crate::{ entities::{ + changes::{Change, LoanMutation}, + input::{PrincipalInput, RepaidInput}, loans::LoanInfo, pricing::{ internal::{InternalPricing, MaxBorrowAmount}, - Pricing, PricingAmount, RepaidPricingAmount, + Pricing, }, }, pallet::*, types::{ valuation::{DiscountedCashFlow, ValuationMethod}, - BorrowRestrictions, InterestPayments, LoanMutation, LoanRestrictions, Maturity, - PayDownSchedule, RepayRestrictions, RepaymentSchedule, + BorrowRestrictions, InterestPayments, LoanRestrictions, Maturity, PayDownSchedule, + RepayRestrictions, RepaymentSchedule, }, }; @@ -185,7 +187,7 @@ where RawOrigin::Signed(borrower).into(), pool_id, loan_id, - PricingAmount::Internal(10.into()), + PrincipalInput::Internal(10.into()), ) .unwrap(); } @@ -196,8 +198,8 @@ where RawOrigin::Signed(borrower).into(), pool_id, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(10.into()), + RepaidInput { + principal: PrincipalInput::Internal(10.into()), interest: T::Balance::max_value(), unscheduled: 0.into(), }, @@ -225,7 +227,7 @@ where // to obtain the ChangeId used previously. T::ChangeGuard::note( pool_id, - ChangeOf::::Loan(loan_id, Self::create_mutation()).into(), + Change::::Loan(loan_id, Self::create_mutation()).into(), ) .unwrap() } @@ -243,7 +245,40 @@ where // We need to call noted again // (that is idempotent for the same change and instant) // to obtain the ChangeId used previously. - T::ChangeGuard::note(pool_id, ChangeOf::::Policy(policy).into()).unwrap() + T::ChangeGuard::note(pool_id, Change::::Policy(policy).into()).unwrap() + } + + fn propose_transfer_debt(pool_id: T::PoolId) -> T::Hash { + let borrower = account("borrower", 0, 0); + let loan_1 = Helper::::create_loan(pool_id, u16::MAX.into()); + Helper::::borrow_loan(pool_id, loan_1); + let loan_2 = Helper::::create_loan(pool_id, (u16::MAX - 1).into()); + + let repaid_amount = RepaidInput { + principal: PrincipalInput::Internal(10.into()), + interest: 0.into(), + unscheduled: 0.into(), + }; + let borrow_amount = PrincipalInput::Internal(10.into()); + + Pallet::::propose_transfer_debt( + RawOrigin::Signed(borrower).into(), + pool_id, + loan_1, + loan_2, + repaid_amount.clone(), + borrow_amount.clone(), + ) + .unwrap(); + + // We need to call noted again + // (that is idempotent for the same change and instant) + // to obtain the ChangeId used previously. + T::ChangeGuard::note( + pool_id, + Change::::TransferDebt(loan_1, loan_2, repaid_amount, borrow_amount).into(), + ) + .unwrap() } fn set_policy(pool_id: T::PoolId) { @@ -255,7 +290,7 @@ where } fn expire_loan(pool_id: T::PoolId, loan_id: T::LoanId) { - Pallet::::expire(pool_id, loan_id).unwrap(); + Pallet::::expire_action(pool_id, loan_id).unwrap(); } fn initialize_active_state(n: u32) -> T::PoolId { @@ -323,7 +358,7 @@ benchmarks! { let pool_id = Helper::::initialize_active_state(n); let loan_id = Helper::::create_loan(pool_id, u16::MAX.into()); - }: _(RawOrigin::Signed(borrower), pool_id, loan_id, PricingAmount::Internal(10.into())) + }: _(RawOrigin::Signed(borrower), pool_id, loan_id, PrincipalInput::Internal(10.into())) repay { let n in 1..Helper::::max_active_loans() - 1; @@ -333,8 +368,8 @@ benchmarks! { let loan_id = Helper::::create_loan(pool_id, u16::MAX.into()); Helper::::borrow_loan(pool_id, loan_id); - let repaid = RepaidPricingAmount { - principal: PricingAmount::Internal(10.into()), + let repaid = RepaidInput { + principal: PrincipalInput::Internal(10.into()), interest: 0.into(), unscheduled: 0.into() }; @@ -423,6 +458,32 @@ benchmarks! { verify { assert!(Pallet::::portfolio_valuation(pool_id).value() > Zero::zero()); } + + propose_transfer_debt { + let n in 2..Helper::::max_active_loans() - 2; + + let borrower = account("borrower", 0, 0); + let pool_id = Helper::::initialize_active_state(n); + let loan_1 = Helper::::create_loan(pool_id, u16::MAX.into()); + Helper::::borrow_loan(pool_id, loan_1); + let loan_2 = Helper::::create_loan(pool_id, (u16::MAX - 1).into()); + + let repaid_amount = RepaidInput { + principal: PrincipalInput::Internal(10.into()), + interest: 0.into(), + unscheduled: 0.into() + }; + let borrow_amount = PrincipalInput::Internal(10.into()); + + }: _(RawOrigin::Signed(borrower), pool_id, loan_1, loan_2, repaid_amount, borrow_amount) + + apply_transfer_debt { + let any = account("any", 0, 0); + let pool_id = Helper::::prepare_benchmark(); + let change_id = Helper::::propose_transfer_debt(pool_id); + + }: _(RawOrigin::Signed(any), pool_id, change_id) + } impl_benchmark_test_suite!( diff --git a/pallets/loans/src/entities/changes.rs b/pallets/loans/src/entities/changes.rs new file mode 100644 index 0000000000..4f7cfdfc1d --- /dev/null +++ b/pallets/loans/src/entities/changes.rs @@ -0,0 +1,43 @@ +use cfg_primitives::Moment; +use cfg_traits::interest::InterestRate; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{storage::bounded_vec::BoundedVec, RuntimeDebug}; +use scale_info::TypeInfo; + +use crate::{ + entities::input::{PrincipalInput, RepaidInput}, + pallet::Config, + types::{ + policy::WriteOffRule, valuation::ValuationMethod, InterestPayments, Maturity, + PayDownSchedule, + }, +}; + +/// Active loan mutation for internal pricing +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] +pub enum InternalMutation { + ValuationMethod(ValuationMethod), + ProbabilityOfDefault(Rate), + LossGivenDefault(Rate), + DiscountRate(InterestRate), +} + +/// Active loan mutation +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] +pub enum LoanMutation { + Maturity(Maturity), + MaturityExtension(Moment), + InterestRate(InterestRate), + InterestPayments(InterestPayments), + PayDownSchedule(PayDownSchedule), + Internal(InternalMutation), +} + +/// Change description +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub enum Change { + Loan(T::LoanId, LoanMutation), + Policy(BoundedVec, T::MaxWriteOffPolicySize>), + TransferDebt(T::LoanId, T::LoanId, RepaidInput, PrincipalInput), +} diff --git a/pallets/loans/src/entities/input.rs b/pallets/loans/src/entities/input.rs new file mode 100644 index 0000000000..9fba7e0ec3 --- /dev/null +++ b/pallets/loans/src/entities/input.rs @@ -0,0 +1,58 @@ +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::RuntimeDebugNoBound; +use scale_info::TypeInfo; +use sp_runtime::{ArithmeticError, DispatchError}; + +use crate::{ + entities::pricing::external::ExternalAmount, + pallet::{Config, Error}, + types::RepaidAmount, +}; + +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub enum PrincipalInput { + Internal(T::Balance), + External(ExternalAmount), +} + +impl PrincipalInput { + pub fn balance(&self) -> Result { + match self { + Self::Internal(amount) => Ok(*amount), + Self::External(external) => external.balance(), + } + } + + pub fn internal(&self) -> Result { + match self { + Self::Internal(amount) => Ok(*amount), + Self::External(_) => Err(Error::::MismatchedPricingMethod.into()), + } + } + + pub fn external(&self) -> Result, DispatchError> { + match self { + Self::Internal(_) => Err(Error::::MismatchedPricingMethod.into()), + Self::External(principal) => Ok(principal.clone()), + } + } +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct RepaidInput { + pub principal: PrincipalInput, + pub interest: T::Balance, + pub unscheduled: T::Balance, +} + +impl RepaidInput { + pub fn repaid_amount(&self) -> Result, ArithmeticError> { + Ok(RepaidAmount { + principal: self.principal.balance()?, + interest: self.interest, + unscheduled: self.unscheduled, + }) + } +} diff --git a/pallets/loans/src/entities/loans.rs b/pallets/loans/src/entities/loans.rs index 295c59db93..4702b221b4 100644 --- a/pallets/loans/src/entities/loans.rs +++ b/pallets/loans/src/entities/loans.rs @@ -17,17 +17,20 @@ use sp_runtime::{ DispatchError, }; -use super::pricing::{ - external::ExternalActivePricing, internal::InternalActivePricing, ActivePricing, Pricing, - PricingAmount, RepaidPricingAmount, -}; use crate::{ + entities::{ + changes::LoanMutation, + input::{PrincipalInput, RepaidInput}, + pricing::{ + external::ExternalActivePricing, internal::InternalActivePricing, ActivePricing, + Pricing, + }, + }, pallet::{AssetOf, Config, Error, PriceOf}, types::{ policy::{WriteOffStatus, WriteOffTrigger}, - BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanMutation, - LoanRestrictions, MutationError, RepaidAmount, RepayLoanError, RepayRestrictions, - RepaymentSchedule, + BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, LoanRestrictions, + MutationError, RepaidAmount, RepayLoanError, RepayRestrictions, RepaymentSchedule, }, }; @@ -99,7 +102,7 @@ impl CreatedLoan { pub fn activate( self, pool_id: T::PoolId, - initial_amount: PricingAmount, + initial_amount: PrincipalInput, ) -> Result, DispatchError> { ActiveLoan::new( pool_id, @@ -186,7 +189,7 @@ impl ActiveLoan { pool_id: T::PoolId, info: LoanInfo, borrower: T::AccountId, - initial_amount: PricingAmount, + initial_amount: PrincipalInput, now: Moment, ) -> Result { Ok(ActiveLoan { @@ -298,7 +301,7 @@ impl ActiveLoan { self.write_down(value) } - fn ensure_can_borrow(&self, amount: &PricingAmount, pool_id: T::PoolId) -> DispatchResult { + fn ensure_can_borrow(&self, amount: &PrincipalInput, pool_id: T::PoolId) -> DispatchResult { let max_borrow_amount = match &self.pricing { ActivePricing::Internal(inner) => { amount.internal()?; @@ -340,7 +343,7 @@ impl ActiveLoan { Ok(()) } - pub fn borrow(&mut self, amount: &PricingAmount, pool_id: T::PoolId) -> DispatchResult { + pub fn borrow(&mut self, amount: &PrincipalInput, pool_id: T::PoolId) -> DispatchResult { self.ensure_can_borrow(amount, pool_id)?; self.total_borrowed.ensure_add_assign(amount.balance()?)?; @@ -365,9 +368,9 @@ impl ActiveLoan { /// - Checking repay restrictions fn prepare_repayment( &self, - mut amount: RepaidPricingAmount, + mut amount: RepaidInput, pool_id: T::PoolId, - ) -> Result, DispatchError> { + ) -> Result, DispatchError> { let (max_repay_principal, outstanding_interest) = match &self.pricing { ActivePricing::Internal(inner) => { amount.principal.internal()?; @@ -409,9 +412,9 @@ impl ActiveLoan { pub fn repay( &mut self, - amount: RepaidPricingAmount, + amount: RepaidInput, pool_id: T::PoolId, - ) -> Result, DispatchError> { + ) -> Result, DispatchError> { let amount = self.prepare_repayment(amount, pool_id)?; self.total_repaid diff --git a/pallets/loans/src/entities/pricing.rs b/pallets/loans/src/entities/pricing.rs index 0764e1dd48..1607a24a1e 100644 --- a/pallets/loans/src/entities/pricing.rs +++ b/pallets/loans/src/entities/pricing.rs @@ -1,12 +1,8 @@ use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::RuntimeDebugNoBound; use scale_info::TypeInfo; -use sp_runtime::{ArithmeticError, DispatchError}; -use crate::{ - pallet::{Config, Error}, - types::RepaidAmount, -}; +use crate::pallet::Config; pub mod external; pub mod internal; @@ -32,51 +28,3 @@ pub enum ActivePricing { /// Internal attributes External(external::ExternalActivePricing), } - -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)] -#[scale_info(skip_type_params(T))] -pub enum PricingAmount { - Internal(T::Balance), - External(external::ExternalAmount), -} - -impl PricingAmount { - pub fn balance(&self) -> Result { - match self { - Self::Internal(amount) => Ok(*amount), - Self::External(external) => external.balance(), - } - } - - pub fn internal(&self) -> Result { - match self { - Self::Internal(amount) => Ok(*amount), - Self::External(_) => Err(Error::::MismatchedPricingMethod.into()), - } - } - - pub fn external(&self) -> Result, DispatchError> { - match self { - Self::Internal(_) => Err(Error::::MismatchedPricingMethod.into()), - Self::External(principal) => Ok(principal.clone()), - } - } -} - -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen)] -#[scale_info(skip_type_params(T))] -pub struct RepaidPricingAmount { - pub principal: PricingAmount, - pub interest: T::Balance, - pub unscheduled: T::Balance, -} - -impl RepaidPricingAmount { - pub fn repaid_amount(&self) -> Result, ArithmeticError> { - Ok(RepaidAmount { - principal: self.principal.balance()?, - interest: self.interest, - unscheduled: self.unscheduled, - }) - } -} diff --git a/pallets/loans/src/entities/pricing/internal.rs b/pallets/loans/src/entities/pricing/internal.rs index b1efa7b6ac..d43c3479b9 100644 --- a/pallets/loans/src/entities/pricing/internal.rs +++ b/pallets/loans/src/entities/pricing/internal.rs @@ -13,11 +13,11 @@ use sp_runtime::{ }; use crate::{ - entities::interest::ActiveInterestRate, + entities::{changes::InternalMutation, interest::ActiveInterestRate}, pallet::{Config, Error}, types::{ valuation::{DiscountedCashFlow, ValuationMethod}, - CreateLoanError, InternalMutation, MutationError, + CreateLoanError, MutationError, }, }; diff --git a/pallets/loans/src/lib.rs b/pallets/loans/src/lib.rs index 4ff5063295..6afd12e67c 100644 --- a/pallets/loans/src/lib.rs +++ b/pallets/loans/src/lib.rs @@ -26,6 +26,8 @@ //! | [`Pallet::admin_write_off()`] | LoanAdmin | //! | [`Pallet::propose_loan_mutation()`] | LoanAdmin | //! | [`Pallet::apply_loan_mutation()`] | | +//! | [`Pallet::propose_transfer_debt()`] | Borrower | +//! | [`Pallet::apply_transfer_debt()`] | | //! | [`Pallet::close()`] | Borrower | //! //! The following actions are performed over an entire pool of loans: @@ -42,6 +44,8 @@ /// High level types that uses `pallet::Config` pub mod entities { + pub mod changes; + pub mod input; pub mod interest; pub mod loans; pub mod pricing; @@ -77,8 +81,9 @@ pub mod pallet { }; use codec::HasCompact; use entities::{ + changes::{Change, LoanMutation}, + input::{PrincipalInput, RepaidInput}, loans::{self, ActiveLoan, ActiveLoanInfo, LoanInfo}, - pricing::{PricingAmount, RepaidPricingAmount}, }; use frame_support::{ pallet_prelude::*, @@ -103,8 +108,8 @@ pub mod pallet { self, policy::{self, WriteOffRule, WriteOffStatus}, portfolio::{self, InitialPortfolioValuation, PortfolioValuationUpdateType}, - BorrowLoanError, Change, CloseLoanError, CreateLoanError, LoanMutation, MutationError, - RepayLoanError, WrittenOffError, + BorrowLoanError, CloseLoanError, CreateLoanError, MutationError, RepayLoanError, + WrittenOffError, }; use super::*; @@ -112,8 +117,6 @@ pub mod pallet { pub type PortfolioInfoOf = Vec<(::LoanId, ActiveLoanInfo)>; pub type AssetOf = (::CollectionId, ::ItemId); pub type PriceOf = (::Balance, Moment); - pub type ChangeOf = - Change<::LoanId, ::Rate, ::MaxWriteOffPolicySize>; const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); @@ -127,7 +130,7 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Represent a runtime change - type RuntimeChange: From> + TryInto>; + type RuntimeChange: From> + TryInto>; /// Identify a curreny. type CurrencyId: Parameter + Copy + MaxEncodedLen; @@ -301,13 +304,13 @@ pub mod pallet { Borrowed { pool_id: T::PoolId, loan_id: T::LoanId, - amount: PricingAmount, + amount: PrincipalInput, }, /// An amount was repaid for a loan Repaid { pool_id: T::PoolId, loan_id: T::LoanId, - amount: RepaidPricingAmount, + amount: RepaidInput, }, /// A loan was written off WrittenOff { @@ -327,16 +330,24 @@ pub mod pallet { loan_id: T::LoanId, collateral: AssetOf, }, - /// The Portfolio Valuation for a pool was updated. + /// The portfolio valuation for a pool was updated. PortfolioValuationUpdated { pool_id: T::PoolId, valuation: T::Balance, update_type: PortfolioValuationUpdateType, }, + /// The write off policy for a pool was updated. WriteOffPolicyUpdated { pool_id: T::PoolId, policy: BoundedVec, T::MaxWriteOffPolicySize>, }, + /// Debt has been transfered between loans + DebtTransferred { + pool_id: T::PoolId, + from_loan_id: T::LoanId, + to_loan_id: T::LoanId, + amount: T::Balance, + }, } #[pallet::error] @@ -376,6 +387,10 @@ pub mod pallet { CloseLoanError(CloseLoanError), /// Emits when the loan can not be mutated MutationError(MutationError), + /// Emits when debt is transfered to the same loan + TransferDebtToSameLoan, + /// Emits when debt is transfered with different repaid/borrow amounts + TransferDebtAmountMismatched, } impl From for Error { @@ -463,27 +478,11 @@ pub mod pallet { origin: OriginFor, pool_id: T::PoolId, loan_id: T::LoanId, - amount: PricingAmount, + amount: PrincipalInput, ) -> DispatchResult { let who = ensure_signed(origin)?; - let _count = match CreatedLoan::::take(pool_id, loan_id) { - Some(created_loan) => { - Self::ensure_loan_borrower(&who, created_loan.borrower())?; - - let mut active_loan = created_loan.activate(pool_id, amount.clone())?; - active_loan.borrow(&amount, pool_id)?; - - Self::insert_active_loan(pool_id, loan_id, active_loan)? - } - None => { - Self::update_active_loan(pool_id, loan_id, |loan| { - Self::ensure_loan_borrower(&who, loan.borrower())?; - loan.borrow(&amount, pool_id) - })? - .1 - } - }; + let _count = Self::borrow_action(&who, pool_id, loan_id, &amount, false)?; T::Pool::withdraw(pool_id, who, amount.balance()?)?; @@ -512,14 +511,11 @@ pub mod pallet { origin: OriginFor, pool_id: T::PoolId, loan_id: T::LoanId, - amount: RepaidPricingAmount, + amount: RepaidInput, ) -> DispatchResult { let who = ensure_signed(origin)?; - let (amount, _count) = Self::update_active_loan(pool_id, loan_id, |loan| { - Self::ensure_loan_borrower(&who, loan.borrower())?; - loan.repay(amount, pool_id) - })?; + let (amount, _count) = Self::repay_action(&who, pool_id, loan_id, &amount, false)?; T::Pool::deposit(pool_id, who, amount.repaid_amount()?.total()?)?; @@ -770,6 +766,174 @@ pub mod pallet { Ok(Some(T::WeightInfo::update_portfolio_valuation(count)).into()) } + + /// Transfer debt from one loan to another loan, + /// repaying from the first loan and borrowing the same amount from the + /// second loan. `from_loan_id` is the loan used to repay. + /// `to_loan_id` is the loan used to borrow. + /// The repaid and borrow amount must match. + #[pallet::weight(T::WeightInfo::propose_transfer_debt(T::MaxActiveLoansPerPool::get()))] + #[pallet::call_index(11)] + pub fn propose_transfer_debt( + origin: OriginFor, + pool_id: T::PoolId, + from_loan_id: T::LoanId, + to_loan_id: T::LoanId, + repaid_amount: RepaidInput, + borrow_amount: PrincipalInput, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + transactional::with_transaction(|| { + let result = Self::transfer_debt_action( + &who, + pool_id, + from_loan_id, + to_loan_id, + repaid_amount.clone(), + borrow_amount.clone(), + false, + ); + + // We do not want to apply the mutation, + // only check if there is no error in applying it + TransactionOutcome::Rollback(result) + })?; + + T::ChangeGuard::note( + pool_id, + Change::TransferDebt(from_loan_id, to_loan_id, repaid_amount, borrow_amount).into(), + )?; + + Ok(()) + } + + /// Transfer debt from one loan to another loan, + /// repaying from the first loan and borrowing the same amount from the + /// second loan. `from_loan_id` is the loan used to repay. + /// `to_loan_id` is the loan used to borrow. + /// The repaid and borrow amount must match. + #[pallet::weight(T::WeightInfo::apply_transfer_debt(T::MaxActiveLoansPerPool::get()))] + #[pallet::call_index(12)] + pub fn apply_transfer_debt( + origin: OriginFor, + pool_id: T::PoolId, + change_id: T::Hash, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let Change::TransferDebt(from_loan_id, to_loan_id, repaid_amount, borrow_amount) = + Self::get_released_change(pool_id, change_id)? else { + Err(Error::::UnrelatedChangeId)? + }; + + let (amount, _count) = Self::transfer_debt_action( + &who, + pool_id, + from_loan_id, + to_loan_id, + repaid_amount, + borrow_amount, + true, + )?; + + Self::deposit_event(Event::::DebtTransferred { + pool_id, + from_loan_id, + to_loan_id, + amount, + }); + + Ok(()) + } + } + + // Loan actions + impl Pallet { + fn borrow_action( + who: &T::AccountId, + pool_id: T::PoolId, + loan_id: T::LoanId, + amount: &PrincipalInput, + permissionless: bool, + ) -> Result { + Ok(match CreatedLoan::::take(pool_id, loan_id) { + Some(created_loan) => { + if !permissionless { + Self::ensure_loan_borrower(who, created_loan.borrower())?; + } + + let mut active_loan = created_loan.activate(pool_id, amount.clone())?; + active_loan.borrow(amount, pool_id)?; + + Self::insert_active_loan(pool_id, loan_id, active_loan)? + } + None => { + Self::update_active_loan(pool_id, loan_id, |loan| { + if !permissionless { + Self::ensure_loan_borrower(who, loan.borrower())?; + } + + loan.borrow(amount, pool_id) + })? + .1 + } + }) + } + + fn repay_action( + who: &T::AccountId, + pool_id: T::PoolId, + loan_id: T::LoanId, + amount: &RepaidInput, + permissionless: bool, + ) -> Result<(RepaidInput, u32), DispatchError> { + Self::update_active_loan(pool_id, loan_id, |loan| { + if !permissionless { + Self::ensure_loan_borrower(who, loan.borrower())?; + } + + loan.repay(amount.clone(), pool_id) + }) + } + + fn transfer_debt_action( + who: &T::AccountId, + pool_id: T::PoolId, + from_loan_id: T::LoanId, + to_loan_id: T::LoanId, + repaid_amount: RepaidInput, + borrow_amount: PrincipalInput, + permissionless: bool, + ) -> Result<(T::Balance, u32), DispatchError> { + ensure!( + from_loan_id != to_loan_id, + Error::::TransferDebtToSameLoan + ); + + let repaid_amount = + Self::repay_action(who, pool_id, from_loan_id, &repaid_amount, permissionless)?.0; + + ensure!( + borrow_amount.balance()? == repaid_amount.repaid_amount()?.total()?, + Error::::TransferDebtAmountMismatched + ); + + let count = + Self::borrow_action(who, pool_id, to_loan_id, &borrow_amount, permissionless)?; + + Ok((repaid_amount.repaid_amount()?.total()?, count)) + } + + /// Set the maturity date of the loan to this instant. + #[cfg(feature = "runtime-benchmarks")] + pub fn expire_action(pool_id: T::PoolId, loan_id: T::LoanId) -> DispatchResult { + Self::update_active_loan(pool_id, loan_id, |loan| { + loan.set_maturity(T::Time::now().as_secs()); + Ok(()) + })?; + Ok(()) + } } /// Utility methods @@ -842,7 +1006,7 @@ pub mod pallet { fn get_released_change( pool_id: T::PoolId, change_id: T::Hash, - ) -> Result, DispatchError> { + ) -> Result, DispatchError> { T::ChangeGuard::released(pool_id, change_id)? .try_into() .map_err(|_| Error::::NoLoanChangeId.into()) @@ -991,16 +1155,6 @@ pub mod pallet { .map(|(_, loan)| (pool_id, loan).try_into()) .transpose() } - - /// Set the maturity date of the loan to this instant. - #[cfg(feature = "runtime-benchmarks")] - pub fn expire(pool_id: T::PoolId, loan_id: T::LoanId) -> DispatchResult { - Self::update_active_loan(pool_id, loan_id, |loan| { - loan.set_maturity(T::Time::now().as_secs()); - Ok(()) - })?; - Ok(()) - } } // TODO: This implementation can be cleaned once #908 be solved diff --git a/pallets/loans/src/tests/borrow_loan.rs b/pallets/loans/src/tests/borrow_loan.rs index 4e2bd70f5d..5421e8edfb 100644 --- a/pallets/loans/src/tests/borrow_loan.rs +++ b/pallets/loans/src/tests/borrow_loan.rs @@ -34,7 +34,7 @@ fn with_wrong_loan_id() { RuntimeOrigin::signed(BORROWER), POOL_A, 0, - PricingAmount::Internal(COLLATERAL_VALUE) + PrincipalInput::Internal(COLLATERAL_VALUE) ), Error::::LoanNotActiveOrNotFound ); @@ -53,7 +53,7 @@ fn from_other_borrower() { RuntimeOrigin::signed(OTHER_BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE) + PrincipalInput::Internal(COLLATERAL_VALUE) ), Error::::NotLoanBorrower ); @@ -70,7 +70,7 @@ fn with_restriction_no_written_off() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE / 2) + PrincipalInput::Internal(COLLATERAL_VALUE / 2) )); advance_time(YEAR + DAY); @@ -81,7 +81,7 @@ fn with_restriction_no_written_off() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE / 2) + PrincipalInput::Internal(COLLATERAL_VALUE / 2) ), Error::::from(BorrowLoanError::Restriction) ); @@ -105,7 +105,7 @@ fn with_restriction_full_once() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE / 2) // Must be full value + PrincipalInput::Internal(COLLATERAL_VALUE / 2) // Must be full value ), Error::::from(BorrowLoanError::Restriction) ); @@ -115,7 +115,7 @@ fn with_restriction_full_once() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE) + PrincipalInput::Internal(COLLATERAL_VALUE) )); // Borrow was already done @@ -124,7 +124,7 @@ fn with_restriction_full_once() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(0) + PrincipalInput::Internal(0) ), Error::::from(BorrowLoanError::Restriction) ); @@ -144,7 +144,7 @@ fn with_maturity_passed() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE) + PrincipalInput::Internal(COLLATERAL_VALUE) ), Error::::from(BorrowLoanError::MaturityDatePassed) ); @@ -162,7 +162,7 @@ fn with_wrong_internal_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(0) + PrincipalInput::Internal(0) ), Error::::MismatchedPricingMethod ); @@ -180,7 +180,7 @@ fn with_wrong_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(ExternalAmount::empty()) + PrincipalInput::External(ExternalAmount::empty()) ), Error::::MismatchedPricingMethod ); @@ -214,7 +214,7 @@ fn with_wrong_big_amount_internal_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(amount) + PrincipalInput::Internal(amount) ), Error::::from(BorrowLoanError::MaxAmountExceeded) ); @@ -248,7 +248,7 @@ fn with_correct_amount_internal_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(amount) + PrincipalInput::Internal(amount) )); assert_eq!(amount, util::current_loan_debt(loan_id)); }); @@ -280,7 +280,7 @@ fn with_unregister_price_id_and_oracle_required() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) ), PRICE_ID_NO_FOUND ); @@ -308,7 +308,7 @@ fn with_unregister_price_id_and_oracle_not_required() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) )); assert_eq!( @@ -324,7 +324,7 @@ fn with_unregister_price_id_and_oracle_not_required() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) )); assert_eq!( @@ -355,7 +355,7 @@ fn with_wrong_big_amount_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) ), Error::::from(BorrowLoanError::MaxAmountExceeded) ); @@ -375,7 +375,7 @@ fn with_incorrect_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) ), Error::::SettlementPriceExceedsVariation ); @@ -391,7 +391,7 @@ fn with_incorrect_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) ), Error::::SettlementPriceExceedsVariation ); @@ -407,7 +407,7 @@ fn with_incorrect_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) ), Error::::SettlementPriceExceedsVariation ); @@ -430,7 +430,7 @@ fn with_correct_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) )); assert_eq!( @@ -446,7 +446,7 @@ fn with_correct_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) )); assert_eq!( @@ -465,7 +465,7 @@ fn with_correct_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) )); assert_eq!( @@ -495,7 +495,7 @@ fn with_unlimited_amount_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::External(amount) + PrincipalInput::External(amount) )); }); } @@ -511,7 +511,7 @@ fn twice() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE / 2) + PrincipalInput::Internal(COLLATERAL_VALUE / 2) )); assert_eq!(COLLATERAL_VALUE / 2, util::current_loan_debt(loan_id)); @@ -519,7 +519,7 @@ fn twice() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE / 2) + PrincipalInput::Internal(COLLATERAL_VALUE / 2) )); assert_eq!(COLLATERAL_VALUE, util::current_loan_debt(loan_id)); @@ -530,7 +530,7 @@ fn twice() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(extra) + PrincipalInput::Internal(extra) ), Error::::from(BorrowLoanError::MaxAmountExceeded) ); @@ -548,7 +548,7 @@ fn twice_with_elapsed_time() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE / 2) + PrincipalInput::Internal(COLLATERAL_VALUE / 2) )); assert_eq!(COLLATERAL_VALUE / 2, util::current_loan_debt(loan_id)); @@ -566,7 +566,7 @@ fn twice_with_elapsed_time() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(COLLATERAL_VALUE / 2) + PrincipalInput::Internal(COLLATERAL_VALUE / 2) )); // At this point the loan has been fully borrowed. @@ -576,7 +576,7 @@ fn twice_with_elapsed_time() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - PricingAmount::Internal(extra) + PrincipalInput::Internal(extra) ), Error::::from(BorrowLoanError::MaxAmountExceeded) ); diff --git a/pallets/loans/src/tests/close_loan.rs b/pallets/loans/src/tests/close_loan.rs index 5acd2a46ac..307603e4a0 100644 --- a/pallets/loans/src/tests/close_loan.rs +++ b/pallets/loans/src/tests/close_loan.rs @@ -25,8 +25,8 @@ fn with_wrong_borrower() { ); // Make the loan active and ready to be closed - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); - util::repay_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); + util::repay_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); assert_noop!( Loans::close(RuntimeOrigin::signed(OTHER_BORROWER), POOL_A, loan_id), @@ -39,8 +39,8 @@ fn with_wrong_borrower() { fn without_fully_repaid_internal() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); - util::repay_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE / 2)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); + util::repay_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE / 2)); assert_noop!( Loans::close(RuntimeOrigin::signed(BORROWER), POOL_A, loan_id), @@ -54,9 +54,9 @@ fn without_fully_repaid_external() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); let amount = ExternalAmount::new(QUANTITY / 2.into(), PRICE_VALUE); - util::repay_loan(loan_id, PricingAmount::External(amount)); + util::repay_loan(loan_id, PrincipalInput::External(amount)); assert_noop!( Loans::close(RuntimeOrigin::signed(BORROWER), POOL_A, loan_id), @@ -69,8 +69,8 @@ fn without_fully_repaid_external() { fn with_time_after_fully_repaid_internal() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); - util::repay_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); + util::repay_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR); @@ -88,8 +88,8 @@ fn with_time_after_fully_repaid_internal() { fn with_fully_repaid_internal() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); - util::repay_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); + util::repay_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); assert_ok!(Loans::close( RuntimeOrigin::signed(BORROWER), @@ -106,8 +106,8 @@ fn with_fully_repaid_external() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount.clone())); - util::repay_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount.clone())); + util::repay_loan(loan_id, PrincipalInput::External(amount)); config_mocks(); assert_ok!(Loans::close( diff --git a/pallets/loans/src/tests/mock.rs b/pallets/loans/src/tests/mock.rs index 23ecdc634b..77d04970ba 100644 --- a/pallets/loans/src/tests/mock.rs +++ b/pallets/loans/src/tests/mock.rs @@ -33,7 +33,7 @@ use sp_runtime::{ DispatchError, FixedU128, }; -use crate::{pallet as pallet_loans, ChangeOf}; +use crate::{entities::changes::Change, pallet as pallet_loans}; pub const BLOCK_TIME: Duration = Duration::from_secs(10); pub const YEAR: Duration = Duration::from_secs(365 * 24 * 3600); @@ -55,6 +55,7 @@ pub const ASSET_AA: Asset = (COLLECTION_A, 1); pub const ASSET_AB: Asset = (COLLECTION_A, 2); pub const ASSET_BA: Asset = (COLLECTION_B, 1); pub const ASSET_BB: Asset = (COLLECTION_B, 2); +pub const ASSET_BC: Asset = (COLLECTION_B, 3); pub const NO_ASSET: Asset = (42, 1); pub const POOL_A: PoolId = 1; @@ -218,7 +219,7 @@ impl pallet_mock_data::Config for Runtime { } impl pallet_mock_change_guard::Config for Runtime { - type Change = ChangeOf; + type Change = Change; type ChangeId = H256; type PoolId = PoolId; } @@ -242,7 +243,7 @@ impl pallet_loans::Config for Runtime { type PriceRegistry = MockPrices; type Quantity = Quantity; type Rate = Rate; - type RuntimeChange = ChangeOf; + type RuntimeChange = Change; type RuntimeEvent = RuntimeEvent; type Time = Timer; type WeightInfo = (); @@ -264,6 +265,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { Uniques::create_collection(&COLLECTION_B, &BORROWER, &ASSET_COLLECTION_OWNER).unwrap(); Uniques::mint_into(&COLLECTION_B, &ASSET_BA.1, &BORROWER).unwrap(); Uniques::mint_into(&COLLECTION_B, &ASSET_BB.1, &BORROWER).unwrap(); + Uniques::mint_into(&COLLECTION_B, &ASSET_BC.1, &OTHER_BORROWER).unwrap(); }); ext } diff --git a/pallets/loans/src/tests/mod.rs b/pallets/loans/src/tests/mod.rs index d5d84b544d..ac8dd0011c 100644 --- a/pallets/loans/src/tests/mod.rs +++ b/pallets/loans/src/tests/mod.rs @@ -9,21 +9,22 @@ use sp_runtime::{traits::BadOrigin, DispatchError, FixedPointNumber}; use super::{ entities::{ + changes::{Change, InternalMutation, LoanMutation}, + input::{PrincipalInput, RepaidInput}, loans::{ActiveLoan, LoanInfo}, pricing::{ external::{ExternalAmount, ExternalPricing, MaxBorrowAmount as ExtMaxBorrowAmount}, internal::{InternalPricing, MaxBorrowAmount as IntMaxBorrowAmount}, - ActivePricing, Pricing, PricingAmount, RepaidPricingAmount, + ActivePricing, Pricing, }, }, - pallet::{ActiveLoans, Error, LastLoanId, PortfolioValuation}, + pallet::{ActiveLoans, CreatedLoan, Error, LastLoanId, PortfolioValuation}, types::{ policy::{WriteOffRule, WriteOffStatus, WriteOffTrigger}, valuation::{DiscountedCashFlow, ValuationMethod}, - BorrowLoanError, BorrowRestrictions, Change, CloseLoanError, CreateLoanError, - InterestPayments, InternalMutation, LoanMutation, LoanRestrictions, Maturity, - MutationError, PayDownSchedule, RepayLoanError, RepayRestrictions, RepaymentSchedule, - WrittenOffError, + BorrowLoanError, BorrowRestrictions, CloseLoanError, CreateLoanError, InterestPayments, + LoanRestrictions, Maturity, MutationError, PayDownSchedule, RepayLoanError, + RepayRestrictions, RepaymentSchedule, WrittenOffError, }, }; @@ -37,5 +38,6 @@ mod mutate_loan; mod policy; mod portfolio_valuation; mod repay_loan; +mod transfer_debt; mod util; mod write_off_loan; diff --git a/pallets/loans/src/tests/mutate_loan.rs b/pallets/loans/src/tests/mutate_loan.rs index 9fba088e5c..b85964c263 100644 --- a/pallets/loans/src/tests/mutate_loan.rs +++ b/pallets/loans/src/tests/mutate_loan.rs @@ -50,7 +50,7 @@ fn without_active_loan() { fn with_wrong_policy_change() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(0)); + util::borrow_loan(loan_id, PrincipalInput::Internal(0)); config_mocks(loan_id, &DEFAULT_MUTATION); MockChangeGuard::mock_released(|_, _| Ok(Change::Policy(vec![].try_into().unwrap()))); @@ -66,7 +66,7 @@ fn with_wrong_policy_change() { fn with_wrong_permissions() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(0)); + util::borrow_loan(loan_id, PrincipalInput::Internal(0)); config_mocks(loan_id, &DEFAULT_MUTATION); assert_noop!( @@ -98,7 +98,7 @@ mod wrong_mutation { fn with_dcf() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(0)); + util::borrow_loan(loan_id, PrincipalInput::Internal(0)); let mutation = LoanMutation::Internal(InternalMutation::ProbabilityOfDefault( Rate::from_float(0.5), @@ -121,7 +121,7 @@ mod wrong_mutation { fn with_internal() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); - util::borrow_loan(loan_id, PricingAmount::External(ExternalAmount::empty())); + util::borrow_loan(loan_id, PrincipalInput::External(ExternalAmount::empty())); let mutation = LoanMutation::Internal(InternalMutation::ProbabilityOfDefault( Rate::from_float(0.5), @@ -144,7 +144,7 @@ mod wrong_mutation { fn with_maturity_extension() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(0)); + util::borrow_loan(loan_id, PrincipalInput::Internal(0)); let mutation = LoanMutation::MaturityExtension(YEAR.as_secs()); @@ -165,7 +165,7 @@ mod wrong_mutation { fn with_interest_rate() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(0)); + util::borrow_loan(loan_id, PrincipalInput::Internal(0)); // Too high let mutation = LoanMutation::InterestRate(InterestRate::Fixed { @@ -191,7 +191,7 @@ mod wrong_mutation { fn with_successful_proposal() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(0)); + util::borrow_loan(loan_id, PrincipalInput::Internal(0)); config_mocks(loan_id, &DEFAULT_MUTATION); @@ -235,7 +235,7 @@ fn with_successful_mutation_application() { }; let loan_id = util::create_loan(loan); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE / 2)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE / 2)); let mutations = vec![ // LoanMutation::InterestPayments(..), No changes, only one variant diff --git a/pallets/loans/src/tests/policy.rs b/pallets/loans/src/tests/policy.rs index 3614f249c5..f6d1b8f2b4 100644 --- a/pallets/loans/src/tests/policy.rs +++ b/pallets/loans/src/tests/policy.rs @@ -128,7 +128,7 @@ fn with_price_outdated() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); let policy: BoundedVec<_, _> = vec![WriteOffRule::new( [WriteOffTrigger::PriceOutdated(10)], @@ -177,7 +177,7 @@ fn with_price_outdated() { fn with_success() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); let policy: BoundedVec<_, _> = vec![ WriteOffRule::new( diff --git a/pallets/loans/src/tests/portfolio_valuation.rs b/pallets/loans/src/tests/portfolio_valuation.rs index 2308b07f95..f394e58497 100644 --- a/pallets/loans/src/tests/portfolio_valuation.rs +++ b/pallets/loans/src/tests/portfolio_valuation.rs @@ -72,14 +72,14 @@ fn with_active_loans() { new_test_ext().execute_with(|| { let loan_1 = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_1, PricingAmount::External(amount.clone())); + util::borrow_loan(loan_1, PrincipalInput::External(amount.clone())); let loan_2 = util::create_loan(LoanInfo { collateral: ASSET_BA, ..util::base_internal_loan() }); - util::borrow_loan(loan_2, PricingAmount::Internal(COLLATERAL_VALUE)); - util::repay_loan(loan_2, PricingAmount::Internal(COLLATERAL_VALUE / 4)); + util::borrow_loan(loan_2, PrincipalInput::Internal(COLLATERAL_VALUE)); + util::repay_loan(loan_2, PrincipalInput::Internal(COLLATERAL_VALUE / 4)); let valuation = amount.balance().unwrap() + COLLATERAL_VALUE - COLLATERAL_VALUE / 4; @@ -100,14 +100,14 @@ fn with_active_written_off_loans() { new_test_ext().execute_with(|| { let loan_1 = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_1, PricingAmount::External(amount)); + util::borrow_loan(loan_1, PrincipalInput::External(amount)); let loan_2 = util::create_loan(LoanInfo { collateral: ASSET_BA, ..util::base_internal_loan() }); - util::borrow_loan(loan_2, PricingAmount::Internal(COLLATERAL_VALUE)); - util::repay_loan(loan_2, PricingAmount::Internal(COLLATERAL_VALUE / 4)); + util::borrow_loan(loan_2, PrincipalInput::Internal(COLLATERAL_VALUE)); + util::repay_loan(loan_2, PrincipalInput::Internal(COLLATERAL_VALUE / 4)); advance_time(YEAR + DAY); @@ -125,14 +125,14 @@ fn filled_and_cleaned() { new_test_ext().execute_with(|| { let loan_1 = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_1, PricingAmount::External(amount.clone())); + util::borrow_loan(loan_1, PrincipalInput::External(amount.clone())); let loan_2 = util::create_loan(LoanInfo { collateral: ASSET_BA, ..util::base_internal_loan() }); - util::borrow_loan(loan_2, PricingAmount::Internal(COLLATERAL_VALUE)); - util::repay_loan(loan_2, PricingAmount::Internal(COLLATERAL_VALUE / 2)); + util::borrow_loan(loan_2, PrincipalInput::Internal(COLLATERAL_VALUE)); + util::repay_loan(loan_2, PrincipalInput::Internal(COLLATERAL_VALUE / 2)); advance_time(YEAR + DAY); @@ -140,8 +140,8 @@ fn filled_and_cleaned() { advance_time(YEAR / 2); - util::repay_loan(loan_1, PricingAmount::External(amount)); - util::repay_loan(loan_2, PricingAmount::Internal(COLLATERAL_VALUE / 2)); + util::repay_loan(loan_1, PrincipalInput::External(amount)); + util::repay_loan(loan_2, PrincipalInput::Internal(COLLATERAL_VALUE / 2)); advance_time(YEAR / 2); @@ -160,7 +160,7 @@ fn filled_and_cleaned() { fn exact_and_inexact_matches() { new_test_ext().execute_with(|| { let loan_1 = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_1, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR / 2); config_mocks(); @@ -168,7 +168,7 @@ fn exact_and_inexact_matches() { // repay_loan() should affect to the portfolio valuation with the same value as // the absolute valuation of the loan - util::repay_loan(loan_1, PricingAmount::Internal(COLLATERAL_VALUE / 2)); + util::repay_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE / 2)); expected_portfolio(util::current_loan_pv(loan_1)); }); } @@ -186,7 +186,7 @@ fn with_unregister_price_id_and_oracle_not_required() { let loan_1 = util::create_loan(loan); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_1, PricingAmount::External(amount.clone())); + util::borrow_loan(loan_1, PrincipalInput::External(amount.clone())); advance_time(YEAR / 2); config_mocks(); diff --git a/pallets/loans/src/tests/repay_loan.rs b/pallets/loans/src/tests/repay_loan.rs index 05a5d095ec..ae3ef974e9 100644 --- a/pallets/loans/src/tests/repay_loan.rs +++ b/pallets/loans/src/tests/repay_loan.rs @@ -31,8 +31,8 @@ fn without_borrow_first() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), interest: u128::MAX, unscheduled: 0, }, @@ -52,8 +52,8 @@ fn with_wrong_loan_id() { RuntimeOrigin::signed(BORROWER), POOL_A, 0, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), interest: u128::MAX, unscheduled: 0, }, @@ -67,7 +67,7 @@ fn with_wrong_loan_id() { fn from_other_borrower() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(COLLATERAL_VALUE); assert_noop!( @@ -75,8 +75,8 @@ fn from_other_borrower() { RuntimeOrigin::signed(OTHER_BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), interest: u128::MAX, unscheduled: 0, }, @@ -90,7 +90,7 @@ fn from_other_borrower() { fn has_been_written_off() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); util::write_off_loan(loan_id); @@ -100,8 +100,8 @@ fn has_been_written_off() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), interest: u128::MAX, unscheduled: 0, }, @@ -113,7 +113,7 @@ fn has_been_written_off() { fn with_wrong_external_pricing() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(0); assert_noop!( @@ -121,8 +121,8 @@ fn with_wrong_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(ExternalAmount::empty()), + RepaidInput { + principal: PrincipalInput::External(ExternalAmount::empty()), interest: 0, unscheduled: 0, }, @@ -137,7 +137,7 @@ fn with_wrong_internal_pricing() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); config_mocks(0); assert_noop!( @@ -145,8 +145,8 @@ fn with_wrong_internal_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(0), + RepaidInput { + principal: PrincipalInput::Internal(0), interest: 0, unscheduled: 0, }, @@ -160,15 +160,15 @@ fn with_wrong_internal_pricing() { fn with_success_half_amount() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE / 2)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE / 2)); config_mocks(COLLATERAL_VALUE / 2); assert_ok!(Loans::repay( RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE / 2), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), interest: 0, unscheduled: 0, }, @@ -181,15 +181,15 @@ fn with_success_half_amount() { fn with_success_total_amount() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(COLLATERAL_VALUE); assert_ok!(Loans::repay( RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), interest: 0, unscheduled: 0, }, @@ -202,7 +202,7 @@ fn with_success_total_amount() { fn with_more_than_required() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(COLLATERAL_VALUE); @@ -211,8 +211,8 @@ fn with_more_than_required() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE * 2), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE * 2), interest: 0, unscheduled: 0, }, @@ -224,8 +224,8 @@ fn with_more_than_required() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), interest: u128::MAX, // Here there is no limit unscheduled: 0, }, @@ -238,8 +238,8 @@ fn with_more_than_required() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(1), // All was already repaid + RepaidInput { + principal: PrincipalInput::Internal(1), // All was already repaid interest: 0, unscheduled: 0, } @@ -250,8 +250,8 @@ fn with_more_than_required() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(0), + RepaidInput { + principal: PrincipalInput::Internal(0), interest: u128::MAX, //Discarded unscheduled: 0, }, @@ -269,7 +269,7 @@ fn with_restriction_full_once() { }, ..util::base_internal_loan() }); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(COLLATERAL_VALUE / 2); assert_noop!( @@ -277,8 +277,8 @@ fn with_restriction_full_once() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE / 2), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), interest: 0, unscheduled: 0, }, @@ -291,8 +291,8 @@ fn with_restriction_full_once() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), interest: 0, unscheduled: 0, }, @@ -303,8 +303,8 @@ fn with_restriction_full_once() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(0), + RepaidInput { + principal: PrincipalInput::Internal(0), interest: 0, unscheduled: 0, } @@ -316,15 +316,15 @@ fn with_restriction_full_once() { fn twice_internal() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(COLLATERAL_VALUE / 2); assert_ok!(Loans::repay( RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE / 2), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), interest: 0, unscheduled: 0, }, @@ -335,8 +335,8 @@ fn twice_internal() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE / 2), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), interest: 0, unscheduled: 0, }, @@ -350,7 +350,7 @@ fn twice_external() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); let amount = ExternalAmount::new(QUANTITY / 2.into(), PRICE_VALUE); config_mocks(amount.balance().unwrap()); @@ -358,8 +358,8 @@ fn twice_external() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -377,8 +377,8 @@ fn twice_external() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(remaining), + RepaidInput { + principal: PrincipalInput::External(remaining), interest: 0, unscheduled: 0, }, @@ -391,15 +391,15 @@ fn twice_external() { fn twice_internal_with_elapsed_time() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(COLLATERAL_VALUE / 2); assert_ok!(Loans::repay( RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE / 2), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), interest: 0, unscheduled: 0, }, @@ -419,8 +419,8 @@ fn twice_internal_with_elapsed_time() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE / 2), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), interest: 0, unscheduled: 0, }, @@ -435,8 +435,8 @@ fn twice_internal_with_elapsed_time() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(0), + RepaidInput { + principal: PrincipalInput::Internal(0), interest: still_to_pay, unscheduled: 0, }, @@ -451,7 +451,7 @@ fn twice_external_with_elapsed_time() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); let amount = ExternalAmount::new(QUANTITY / 2.into(), PRICE_VALUE); config_mocks(amount.balance().unwrap()); @@ -459,8 +459,8 @@ fn twice_external_with_elapsed_time() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -483,8 +483,8 @@ fn twice_external_with_elapsed_time() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(remaining), + RepaidInput { + principal: PrincipalInput::External(remaining), interest: 0, unscheduled: 0, }, @@ -499,8 +499,8 @@ fn twice_external_with_elapsed_time() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(ExternalAmount::empty()), + RepaidInput { + principal: PrincipalInput::External(ExternalAmount::empty()), interest: still_to_pay, unscheduled: 0, }, @@ -520,7 +520,7 @@ fn current_debt_rate_no_increase_if_fully_repaid() { }), ..util::base_internal_loan() }); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR / 2); @@ -529,8 +529,8 @@ fn current_debt_rate_no_increase_if_fully_repaid() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(COLLATERAL_VALUE), + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), interest: u128::MAX, unscheduled: 0, }, @@ -547,7 +547,7 @@ fn external_pricing_goes_up() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE * 2); config_mocks_with_price(amount.balance().unwrap(), PRICE_VALUE * 2); @@ -555,8 +555,8 @@ fn external_pricing_goes_up() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -571,7 +571,7 @@ fn external_pricing_goes_down() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE / 2); config_mocks_with_price(amount.balance().unwrap(), PRICE_VALUE / 2); @@ -579,8 +579,8 @@ fn external_pricing_goes_down() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -594,15 +594,15 @@ fn external_pricing_goes_down() { fn with_unscheduled_repayment_internal() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(1234); assert_ok!(Loans::repay( RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::Internal(0), + RepaidInput { + principal: PrincipalInput::Internal(0), interest: 0, unscheduled: 1234, }, @@ -619,15 +619,15 @@ fn with_unscheduled_repayment_external() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); config_mocks(1234); assert_ok!(Loans::repay( RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(ExternalAmount::empty()), + RepaidInput { + principal: PrincipalInput::External(ExternalAmount::empty()), interest: 0, unscheduled: 1234, }, @@ -647,7 +647,7 @@ fn with_incorrect_settlement_price_external_pricing() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); // Much higher let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE + PRICE_VALUE + 1); @@ -657,8 +657,8 @@ fn with_incorrect_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -677,8 +677,8 @@ fn with_incorrect_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -697,8 +697,8 @@ fn with_incorrect_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -713,7 +713,7 @@ fn with_correct_settlement_price_external_pricing() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); // Higher let amount = ExternalAmount::new( @@ -725,8 +725,8 @@ fn with_correct_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -744,8 +744,8 @@ fn with_correct_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -766,8 +766,8 @@ fn with_correct_settlement_price_external_pricing() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, @@ -791,7 +791,7 @@ fn with_unregister_price_id_and_oracle_not_required() { let loan_id = util::create_loan(loan); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); let amount = ExternalAmount::new(QUANTITY / 2.into(), PRICE_VALUE * 2); config_mocks_with_price(amount.balance().unwrap(), 0 /* unused */); @@ -799,8 +799,8 @@ fn with_unregister_price_id_and_oracle_not_required() { RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, - RepaidPricingAmount { - principal: PricingAmount::External(amount), + RepaidInput { + principal: PrincipalInput::External(amount), interest: 0, unscheduled: 0, }, diff --git a/pallets/loans/src/tests/transfer_debt.rs b/pallets/loans/src/tests/transfer_debt.rs new file mode 100644 index 0000000000..ee7668bd12 --- /dev/null +++ b/pallets/loans/src/tests/transfer_debt.rs @@ -0,0 +1,489 @@ +use super::*; + +fn config_mocks( + transfer: ( + LoanId, + LoanId, + RepaidInput, + PrincipalInput, + ), +) { + MockPrices::mock_get(|id, pool_id| { + assert_eq!(*id, REGISTER_PRICE_ID); + assert_eq!(*pool_id, POOL_A); + Ok((PRICE_VALUE, BLOCK_TIME.as_secs())) + }); + MockPrices::mock_register_id(|id, pool_id| { + assert_eq!(*pool_id, POOL_A); + assert_eq!(*id, REGISTER_PRICE_ID); + Ok(()) + }); + MockChangeGuard::mock_note({ + let (loan_1, loan_2, repay, borrow) = transfer.clone(); + move |pool_id, change| { + assert_eq!(pool_id, POOL_A); + assert_eq!( + change, + Change::TransferDebt(loan_1, loan_2, repay.clone(), borrow.clone()) + ); + Ok(CHANGE_ID) + } + }); + MockChangeGuard::mock_released({ + let (loan_1, loan_2, repay, borrow) = transfer.clone(); + move |pool_id, change_id| { + assert_eq!(pool_id, POOL_A); + assert_eq!(change_id, CHANGE_ID); + Ok(Change::TransferDebt( + loan_1, + loan_2, + repay.clone(), + borrow.clone(), + )) + } + }); +} + +#[test] +fn with_wrong_borrower() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_internal_loan()); + util::borrow_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE)); + + let loan_2 = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..util::base_internal_loan() + }); + + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(OTHER_BORROWER), + POOL_A, + loan_1, + loan_2, + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::Internal(COLLATERAL_VALUE), + ), + Error::::NotLoanBorrower + ); + }); +} + +#[test] +fn with_wrong_borrower_for_repaid_loan() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan_by( + LoanInfo { + collateral: ASSET_BC, + ..util::base_internal_loan() + }, + OTHER_BORROWER, + ); + util::borrow_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE)); + + let loan_2 = util::create_loan(util::base_internal_loan()); + + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(OTHER_BORROWER), + POOL_A, + loan_1, + loan_2, + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::Internal(COLLATERAL_VALUE), + ), + Error::::NotLoanBorrower + ); + }); +} + +#[test] +fn with_wrong_borrower_for_borrowed_loan() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_internal_loan()); + util::borrow_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE)); + + let loan_2 = util::create_loan_by( + LoanInfo { + collateral: ASSET_BC, + ..util::base_internal_loan() + }, + OTHER_BORROWER, + ); + + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(OTHER_BORROWER), + POOL_A, + loan_1, + loan_2, + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::Internal(COLLATERAL_VALUE), + ), + Error::::NotLoanBorrower + ); + }); +} + +#[test] +fn with_wrong_loans() { + new_test_ext().execute_with(|| { + let loan_id = util::create_loan(util::base_internal_loan()); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); + + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + 0, // Does not exists + loan_id, + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::Internal(COLLATERAL_VALUE), + ), + Error::::LoanNotActiveOrNotFound + ); + + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + 0, // Does not exists + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::Internal(COLLATERAL_VALUE), + ), + Error::::LoanNotActiveOrNotFound + ); + }); +} + +#[test] +fn with_same_loan() { + new_test_ext().execute_with(|| { + let loan_id = util::create_loan(util::base_internal_loan()); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); + + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + loan_id, + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::Internal(COLLATERAL_VALUE), + ), + Error::::TransferDebtToSameLoan + ); + }); +} + +#[test] +fn with_mismatch_internal_internal_amounts() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_internal_loan()); + util::borrow_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE)); + + let loan_2 = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..util::base_internal_loan() + }); + + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_1, + loan_2, + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::Internal(COLLATERAL_VALUE / 3), + ), + Error::::TransferDebtAmountMismatched + ); + }); +} + +#[test] +fn with_mismatch_external_internal_amounts() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_external_loan()); + let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); + util::borrow_loan(loan_1, PrincipalInput::External(amount)); + + let loan_2 = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..util::base_internal_loan() + }); + + let repay_amount = ExternalAmount::new(QUANTITY, PRICE_VALUE + 2); + + MockPrices::mock_get(|id, pool_id| { + assert_eq!(*id, REGISTER_PRICE_ID); + assert_eq!(*pool_id, POOL_A); + Ok((PRICE_VALUE, BLOCK_TIME.as_secs())) + }); + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_1, + loan_2, + RepaidInput { + principal: PrincipalInput::External(repay_amount), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::Internal(COLLATERAL_VALUE), + ), + Error::::TransferDebtAmountMismatched + ); + }); +} + +#[test] +fn with_mismatch_internal_external_amounts() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_internal_loan()); + util::borrow_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE)); + + let loan_2 = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..util::base_external_loan() + }); + + let borrow_amount = ExternalAmount::new(QUANTITY, PRICE_VALUE * 3); + + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_1, + loan_2, + RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::External(borrow_amount), + ), + Error::::TransferDebtAmountMismatched + ); + }); +} + +#[test] +fn with_mismatch_external_external_amounts() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_external_loan()); + let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); + util::borrow_loan(loan_1, PrincipalInput::External(amount)); + + let loan_2 = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..util::base_external_loan() + }); + + let repay_amount = ExternalAmount::new(QUANTITY, PRICE_VALUE + 2); + let borrow_amount = ExternalAmount::new(QUANTITY, PRICE_VALUE + 3); + + MockPrices::mock_get(|id, pool_id| { + assert_eq!(*id, REGISTER_PRICE_ID); + assert_eq!(*pool_id, POOL_A); + Ok((PRICE_VALUE, BLOCK_TIME.as_secs())) + }); + assert_noop!( + Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_1, + loan_2, + RepaidInput { + principal: PrincipalInput::External(repay_amount), + interest: 0, + unscheduled: 0, + }, + PrincipalInput::External(borrow_amount), + ), + Error::::TransferDebtAmountMismatched + ); + }); +} + +#[test] +fn apply_without_released() { + new_test_ext().execute_with(|| { + MockChangeGuard::mock_released(|_, _| Err("err".into())); + + assert_noop!( + Loans::apply_transfer_debt(RuntimeOrigin::signed(ANY), POOL_A, CHANGE_ID), + DispatchError::Other("err") + ); + }); +} + +#[test] +fn with_success_internals() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_internal_loan()); + util::borrow_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE)); + + let loan_2 = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..util::base_internal_loan() + }); + + let repay_amount = RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE), + interest: 0, + unscheduled: 0, + }; + let borrow_amount = PrincipalInput::Internal(COLLATERAL_VALUE); + + config_mocks((loan_1, loan_2, repay_amount.clone(), borrow_amount.clone())); + assert_ok!(Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_1, + loan_2, + repay_amount, + borrow_amount, + )); + + assert_ok!(Loans::apply_transfer_debt( + RuntimeOrigin::signed(ANY), + POOL_A, + CHANGE_ID, + )); + + assert_eq!(0, util::current_loan_debt(loan_1)); + assert_eq!(COLLATERAL_VALUE, util::current_loan_debt(loan_2)); + }); +} + +#[test] +fn with_success_externals() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_external_loan()); + let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); + util::borrow_loan(loan_1, PrincipalInput::External(amount)); + + let loan_2 = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..util::base_external_loan() + }); + + let repay_amount = RepaidInput { + principal: PrincipalInput::External(ExternalAmount::new(QUANTITY, PRICE_VALUE)), + interest: 0, + unscheduled: 0, + }; + let borrow_amount = PrincipalInput::External(ExternalAmount::new(QUANTITY, PRICE_VALUE)); + + config_mocks((loan_1, loan_2, repay_amount.clone(), borrow_amount.clone())); + assert_ok!(Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_1, + loan_2, + repay_amount, + borrow_amount, + )); + + assert_ok!(Loans::apply_transfer_debt( + RuntimeOrigin::signed(ANY), + POOL_A, + CHANGE_ID, + )); + + assert_eq!(0, util::current_loan_debt(loan_1)); + assert_eq!( + QUANTITY.saturating_mul_int(NOTIONAL), + util::current_loan_debt(loan_2) + ); + }); +} + +#[test] +fn with_transfer_roundtrip() { + new_test_ext().execute_with(|| { + let loan_1 = util::create_loan(util::base_internal_loan()); + util::borrow_loan(loan_1, PrincipalInput::Internal(COLLATERAL_VALUE / 2)); + + let loan_2 = util::create_loan(LoanInfo { + collateral: ASSET_BA, + ..util::base_internal_loan() + }); + + let repay_amount = RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), + interest: 0, + unscheduled: 0, + }; + let borrow_amount = PrincipalInput::Internal(COLLATERAL_VALUE / 2); + + config_mocks((loan_1, loan_2, repay_amount.clone(), borrow_amount.clone())); + assert_ok!(Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_1, + loan_2, + repay_amount.clone(), + borrow_amount.clone(), + )); + + assert_ok!(Loans::apply_transfer_debt( + RuntimeOrigin::signed(ANY), + POOL_A, + CHANGE_ID, + )); + + assert_eq!(0, util::current_loan_debt(loan_1)); + assert_eq!(COLLATERAL_VALUE / 2, util::current_loan_debt(loan_2)); + + config_mocks((loan_2, loan_1, repay_amount.clone(), borrow_amount.clone())); + assert_ok!(Loans::propose_transfer_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_2, + loan_1, + repay_amount, + borrow_amount, + )); + + assert_ok!(Loans::apply_transfer_debt( + RuntimeOrigin::signed(ANY), + POOL_A, + CHANGE_ID, + )); + + assert_eq!(COLLATERAL_VALUE / 2, util::current_loan_debt(loan_1)); + assert_eq!(0, util::current_loan_debt(loan_2)); + }); +} diff --git a/pallets/loans/src/tests/util.rs b/pallets/loans/src/tests/util.rs index 06b57591be..4e6ebc9889 100644 --- a/pallets/loans/src/tests/util.rs +++ b/pallets/loans/src/tests/util.rs @@ -27,6 +27,18 @@ pub fn current_loan_debt(loan_id: LoanId) -> Balance { } } +pub fn borrower(loan_id: LoanId) -> AccountId { + match CreatedLoan::::get(POOL_A, loan_id) { + Some(created_loan) => *created_loan.borrower(), + None => *ActiveLoans::::get(POOL_A) + .into_iter() + .find(|(id, _)| *id == loan_id) + .expect("loan not found") + .1 + .borrower(), + } +} + pub fn current_loan_pv(loan_id: LoanId) -> Balance { get_loan(loan_id).present_value(POOL_A).unwrap() } @@ -122,12 +134,16 @@ pub fn base_external_loan() -> LoanInfo { } pub fn create_loan(loan: LoanInfo) -> LoanId { + create_loan_by(loan, BORROWER) +} + +pub fn create_loan_by(loan: LoanInfo, borrower: AccountId) -> LoanId { MockPermissions::mock_has(|_, _, _| true); MockPools::mock_pool_exists(|_| true); MockPools::mock_account_for(|_| POOL_A_ACCOUNT); MockPrices::mock_get(|_, _| Ok((PRICE_VALUE, BLOCK_TIME.as_secs()))); - Loans::create(RuntimeOrigin::signed(BORROWER), POOL_A, loan).expect("successful creation"); + Loans::create(RuntimeOrigin::signed(borrower), POOL_A, loan).expect("successful creation"); MockPermissions::mock_has(|_, _, _| panic!("no has() mock")); MockPools::mock_pool_exists(|_| panic!("no pool_exists() mock")); @@ -137,13 +153,13 @@ pub fn create_loan(loan: LoanInfo) -> LoanId { LastLoanId::::get(POOL_A) } -pub fn borrow_loan(loan_id: LoanId, borrow_amount: PricingAmount) { +pub fn borrow_loan(loan_id: LoanId, borrow_amount: PrincipalInput) { MockPools::mock_withdraw(|_, _, _| Ok(())); MockPrices::mock_get(|_, _| Ok((PRICE_VALUE, BLOCK_TIME.as_secs()))); MockPrices::mock_register_id(|_, _| Ok(())); Loans::borrow( - RuntimeOrigin::signed(BORROWER), + RuntimeOrigin::signed(borrower(loan_id)), POOL_A, loan_id, borrow_amount, @@ -155,15 +171,15 @@ pub fn borrow_loan(loan_id: LoanId, borrow_amount: PricingAmount) { MockPrices::mock_register_id(|_, _| panic!("no register_id() mock")); } -pub fn repay_loan(loan_id: LoanId, repay_amount: PricingAmount) { +pub fn repay_loan(loan_id: LoanId, repay_amount: PrincipalInput) { MockPools::mock_deposit(|_, _, _| Ok(())); MockPrices::mock_get(|_, _| Ok((PRICE_VALUE, BLOCK_TIME.as_secs()))); Loans::repay( - RuntimeOrigin::signed(BORROWER), + RuntimeOrigin::signed(borrower(loan_id)), POOL_A, loan_id, - RepaidPricingAmount { + RepaidInput { principal: repay_amount, interest: u128::MAX, unscheduled: 0, @@ -187,7 +203,8 @@ pub fn write_off_loan(loan_id: LoanId) { pub fn close_loan(loan_id: LoanId) { MockPrices::mock_unregister_id(|_, _| Ok(())); - Loans::close(RuntimeOrigin::signed(BORROWER), POOL_A, loan_id).expect("successful clossing"); + Loans::close(RuntimeOrigin::signed(borrower(loan_id)), POOL_A, loan_id) + .expect("successful clossing"); MockPrices::mock_get(|_, _| panic!("no unregister_id() mock")); } diff --git a/pallets/loans/src/tests/write_off_loan.rs b/pallets/loans/src/tests/write_off_loan.rs index 5ce25de621..e9028ad112 100644 --- a/pallets/loans/src/tests/write_off_loan.rs +++ b/pallets/loans/src/tests/write_off_loan.rs @@ -12,7 +12,7 @@ fn config_mocks() { fn without_policy() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); assert_noop!( Loans::write_off(RuntimeOrigin::signed(ANY), POOL_A, loan_id), @@ -36,7 +36,7 @@ fn with_policy_but_not_overdue() { util::set_up_policy(POLICY_PERCENTAGE, POLICY_PENALTY); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + BLOCK_TIME); @@ -54,7 +54,7 @@ fn with_valid_maturity() { util::set_up_policy(POLICY_PERCENTAGE, POLICY_PENALTY); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR / 2); @@ -121,7 +121,7 @@ fn with_wrong_permission() { util::set_up_policy(POLICY_PERCENTAGE, POLICY_PENALTY); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); @@ -145,7 +145,7 @@ fn with_success() { util::set_up_policy(POLICY_PERCENTAGE, POLICY_PENALTY); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); @@ -163,7 +163,7 @@ fn with_admin_success() { util::set_up_policy(POLICY_PERCENTAGE, POLICY_PENALTY); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); @@ -213,7 +213,7 @@ fn with_admin_less_than_policy() { util::set_up_policy(POLICY_PERCENTAGE, POLICY_PENALTY); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); @@ -251,7 +251,7 @@ fn with_policy_change_after() { util::set_up_policy(POLICY_PERCENTAGE, POLICY_PENALTY); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); @@ -283,7 +283,7 @@ fn with_policy_change_after() { fn with_policy_change_after_admin() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); config_mocks(); assert_ok!(Loans::admin_write_off( @@ -320,7 +320,7 @@ fn with_percentage_applied_internal() { util::set_up_policy(POLICY_PERCENTAGE, 0.0); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); @@ -347,7 +347,7 @@ fn with_percentage_applied_external() { let loan_id = util::create_loan(util::base_external_loan()); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - util::borrow_loan(loan_id, PricingAmount::External(amount)); + util::borrow_loan(loan_id, PrincipalInput::External(amount)); advance_time(YEAR + DAY); @@ -378,7 +378,7 @@ fn with_penalty_applied() { util::set_up_policy(0.0, POLICY_PENALTY); let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); @@ -418,7 +418,7 @@ fn with_penalty_applied() { fn fully() { new_test_ext().execute_with(|| { let loan_id = util::create_loan(util::base_internal_loan()); - util::borrow_loan(loan_id, PricingAmount::Internal(COLLATERAL_VALUE)); + util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE)); advance_time(YEAR + DAY); diff --git a/pallets/loans/src/types/mod.rs b/pallets/loans/src/types/mod.rs index 1180723be7..1408ecbdfa 100644 --- a/pallets/loans/src/types/mod.rs +++ b/pallets/loans/src/types/mod.rs @@ -14,12 +14,11 @@ //! Contains base types without Config references use cfg_primitives::Moment; -use cfg_traits::interest::InterestRate; use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::{storage::bounded_vec::BoundedVec, PalletError, RuntimeDebug}; +use frame_support::{PalletError, RuntimeDebug}; use scale_info::TypeInfo; use sp_runtime::{ - traits::{EnsureAdd, EnsureAddAssign, EnsureSubAssign, Get}, + traits::{EnsureAdd, EnsureAddAssign, EnsureSubAssign}, ArithmeticError, }; @@ -27,9 +26,6 @@ pub mod policy; pub mod portfolio; pub mod valuation; -use policy::WriteOffRule; -use valuation::ValuationMethod; - /// Error related to loan creation #[derive(Encode, Decode, TypeInfo, PalletError)] pub enum CreateLoanError { @@ -198,33 +194,6 @@ pub struct LoanRestrictions { pub repayments: RepayRestrictions, } -/// Active loan mutation for internal pricing -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] -pub enum InternalMutation { - ValuationMethod(ValuationMethod), - ProbabilityOfDefault(Rate), - LossGivenDefault(Rate), - DiscountRate(InterestRate), -} - -/// Active loan mutation -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] -pub enum LoanMutation { - Maturity(Maturity), - MaturityExtension(Moment), - InterestRate(InterestRate), - InterestPayments(InterestPayments), - PayDownSchedule(PayDownSchedule), - Internal(InternalMutation), -} - -/// Change description -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] -pub enum Change> { - Loan(LoanId, LoanMutation), - Policy(BoundedVec, MaxRules>), -} - #[derive(Default, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] pub struct RepaidAmount { pub principal: Balance, diff --git a/pallets/loans/src/util.rs b/pallets/loans/src/util.rs index 6835be8116..3adfee9420 100644 --- a/pallets/loans/src/util.rs +++ b/pallets/loans/src/util.rs @@ -19,7 +19,10 @@ use orml_traits::{DataFeeder, DataProvider}; use sp_runtime::{DispatchError, DispatchResult}; use sp_std::marker::PhantomData; -use crate::pallet::{ChangeOf, Config, PriceOf}; +use crate::{ + entities::changes::Change, + pallet::{Config, PriceOf}, +}; const DEFAULT_PRICE_ERR: DispatchError = DispatchError::Other("No configured price registry for pallet-loans"); @@ -79,7 +82,7 @@ const DEFAULT_CHANGE_ERR: DispatchError = pub struct NoLoanChanges(PhantomData); impl ChangeGuard for NoLoanChanges { - type Change = ChangeOf; + type Change = Change; type ChangeId = T::Hash; type PoolId = T::PoolId; diff --git a/pallets/loans/src/weights.rs b/pallets/loans/src/weights.rs index 10afda6354..75c2fae7ee 100644 --- a/pallets/loans/src/weights.rs +++ b/pallets/loans/src/weights.rs @@ -25,6 +25,8 @@ pub trait WeightInfo { fn propose_write_off_policy() -> Weight; fn apply_write_off_policy() -> Weight; fn update_portfolio_valuation(n: u32) -> Weight; + fn propose_transfer_debt(n: u32) -> Weight; + fn apply_transfer_debt(n: u32) -> Weight; } impl WeightInfo for () { @@ -71,4 +73,12 @@ impl WeightInfo for () { fn update_portfolio_valuation(_: u32) -> Weight { Weight::zero() } + + fn propose_transfer_debt(_: u32) -> Weight { + Weight::zero() + } + + fn apply_transfer_debt(_: u32) -> Weight { + Weight::zero() + } } diff --git a/runtime/altair/src/weights/pallet_loans.rs b/runtime/altair/src/weights/pallet_loans.rs index 139a064ce9..98f41e212b 100644 --- a/runtime/altair/src/weights/pallet_loans.rs +++ b/runtime/altair/src/weights/pallet_loans.rs @@ -294,4 +294,14 @@ impl pallet_loans::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn propose_transfer_debt(_: u32) -> Weight { + // Pending to regenerate + Weight::default() + } + + fn apply_transfer_debt(_: u32) -> Weight { + // Pending to regenerate + Weight::default() + } } diff --git a/runtime/centrifuge/src/weights/pallet_loans.rs b/runtime/centrifuge/src/weights/pallet_loans.rs index 8d398a8a30..c88696a83e 100644 --- a/runtime/centrifuge/src/weights/pallet_loans.rs +++ b/runtime/centrifuge/src/weights/pallet_loans.rs @@ -292,4 +292,14 @@ impl pallet_loans::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn propose_transfer_debt(_: u32) -> Weight { + // Pending to regenerate + Weight::default() + } + + fn apply_transfer_debt(_: u32) -> Weight { + // Pending to regenerate + Weight::default() + } } diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 2b97e51757..bbf38d815c 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -232,21 +232,21 @@ pub mod asset_registry { pub mod changes { use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::RuntimeDebug; - use pallet_loans::ChangeOf as LoansChangeOf; + use pallet_loans::entities::changes::Change as LoansChange; use pallet_pool_system::pool_types::changes::PoolChangeProposal; use scale_info::TypeInfo; use sp_runtime::DispatchError; #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum RuntimeChange { - Loan(LoansChangeOf), + Loan(LoansChange), } #[cfg(not(feature = "runtime-benchmarks"))] impl From> for PoolChangeProposal { fn from(RuntimeChange::Loan(loans_change): RuntimeChange) -> Self { use cfg_primitives::SECONDS_PER_WEEK; - use pallet_loans::types::{InternalMutation, LoanMutation}; + use pallet_loans::entities::changes::{InternalMutation, LoanMutation}; use pallet_pool_system::pool_types::changes::Requirement; use sp_std::vec; @@ -257,7 +257,7 @@ pub mod changes { let requirements = match loans_change { // Requirements gathered from // - LoansChangeOf::::Loan(_, loan_mutation) => match loan_mutation { + LoansChange::::Loan(_, loan_mutation) => match loan_mutation { LoanMutation::Maturity(_) => vec![week, blocked], LoanMutation::MaturityExtension(_) => vec![], LoanMutation::InterestPayments(_) => vec![week, blocked], @@ -270,7 +270,8 @@ pub mod changes { InternalMutation::DiscountRate(_) => vec![epoch], }, }, - LoansChangeOf::::Policy(_) => vec![week, blocked], + LoansChange::::Policy(_) => vec![week, blocked], + LoansChange::::TransferDebt(_, _, _, _) => vec![epoch, blocked], }; PoolChangeProposal::new(requirements) @@ -291,17 +292,17 @@ pub mod changes { } /// Used for building CfgChanges in pallet-loans - impl From> for RuntimeChange { - fn from(loan_change: LoansChangeOf) -> RuntimeChange { + impl From> for RuntimeChange { + fn from(loan_change: LoansChange) -> RuntimeChange { RuntimeChange::Loan(loan_change) } } /// Used for recovering LoanChange in pallet-loans - impl TryInto> for RuntimeChange { + impl TryInto> for RuntimeChange { type Error = DispatchError; - fn try_into(self) -> Result, DispatchError> { + fn try_into(self) -> Result, DispatchError> { let RuntimeChange::Loan(loan_change) = self; Ok(loan_change) } @@ -331,17 +332,17 @@ pub mod changes { } /// Used for building CfgChanges in pallet-loans - impl From> for RuntimeChange { - fn from(loan_change: LoansChangeOf) -> RuntimeChange { + impl From> for RuntimeChange { + fn from(loan_change: LoansChange) -> RuntimeChange { Self(loan_change.into()) } } /// Used for recovering LoanChange in pallet-loans - impl TryInto> for RuntimeChange { + impl TryInto> for RuntimeChange { type Error = DispatchError; - fn try_into(self) -> Result, DispatchError> { + fn try_into(self) -> Result, DispatchError> { self.0.try_into() } } diff --git a/runtime/development/src/weights/pallet_loans.rs b/runtime/development/src/weights/pallet_loans.rs index 8d398a8a30..c88696a83e 100644 --- a/runtime/development/src/weights/pallet_loans.rs +++ b/runtime/development/src/weights/pallet_loans.rs @@ -292,4 +292,14 @@ impl pallet_loans::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn propose_transfer_debt(_: u32) -> Weight { + // Pending to regenerate + Weight::default() + } + + fn apply_transfer_debt(_: u32) -> Weight { + // Pending to regenerate + Weight::default() + } } diff --git a/runtime/integration-tests/src/utils/loans.rs b/runtime/integration-tests/src/utils/loans.rs index 8d10d65f07..9a4628b40c 100644 --- a/runtime/integration-tests/src/utils/loans.rs +++ b/runtime/integration-tests/src/utils/loans.rs @@ -20,10 +20,11 @@ use cfg_traits::interest::{CompoundingSchedule, InterestRate}; use cfg_types::fixed_point::Rate; use pallet_loans::{ entities::{ + input::{PrincipalInput, RepaidInput}, loans::LoanInfo, pricing::{ internal::{InternalPricing, MaxBorrowAmount}, - Pricing, PricingAmount, RepaidPricingAmount, + Pricing, }, }, types::{ @@ -185,7 +186,7 @@ pub fn create_loan_call(pool_id: PoolId, info: LoanInfo) -> RuntimeCall pub fn borrow_call( pool_id: PoolId, loan_id: LoanId, - amount: PricingAmount, + amount: PrincipalInput, ) -> RuntimeCall { RuntimeCall::Loans(LoansCall::borrow { pool_id, @@ -194,11 +195,7 @@ pub fn borrow_call( }) } -pub fn repay_call( - pool_id: PoolId, - loan_id: LoanId, - amount: RepaidPricingAmount, -) -> RuntimeCall { +pub fn repay_call(pool_id: PoolId, loan_id: LoanId, amount: RepaidInput) -> RuntimeCall { RuntimeCall::Loans(LoansCall::repay { pool_id, loan_id, From 0387385954349e3ca8b2c4d84e4e17b16bbbc5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Enrique=20Mu=C3=B1oz=20Mart=C3=ADn?= Date: Thu, 28 Sep 2023 11:04:26 +0200 Subject: [PATCH 2/2] Github codeowners/templates changes (#1491) * github changes * restore github templates, remove old members * rename to hieronx --- .github/CODEOWNERS | 33 +++++++++++++++----------------- .github/pull_request_template.md | 4 ++-- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b525bfd297..1032fd17e8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,37 +1,34 @@ # Centrifudge Code Owners ## Changes to .github -.github/* @mikiquantum @mustermeiszer @branan @NunoAlexandre +.github/* @mustermeiszer @NunoAlexandre @lemunozm ## Changes to ci -ci/* @mikiquantum @mustermeiszer @branan @NunoAlexandre +ci/* @mustermeiszer @NunoAlexandre @lemunozm ## Changes to the service of our chain. -node/* @mikiquantum @mustermeiszer @branan @NunoAlexandre +node/* @mustermeiszer @NunoAlexandre ## Changes to chain-specs -node/res/* @mikiquantum @mustermeiszer @branan @NunoAlexandre @wischli - -## Changes to toml files -*.toml @mikiquantum @mustermeiszer @branan @NunoAlexandre @lemunozm @wischli @cdamian @thea-leake +node/res/* @mustermeiszer @NunoAlexandre @wischli ## Changes to specific pallets pallets/liquidity-pools/* @NunoAlexandre @cdamian @wischli @mustermeiszer -pallets/rewards/* @lemunozm @mikiquantum -pallets/liquidity-rewards/* @lemunozm @mikiquantum +pallets/rewards/* @lemunozm +pallets/liquidity-rewards/* @lemunozm pallets/block-rewards/* @wischli @lemunozm pallets/nft-sales/* @NunoAlexandre -pallets/keystore/* @cdamian @mikiquantum +pallets/keystore/* @cdamian pallets/pool-registry/* @mustermeiszer -pallets/pool-system/* @mustermeiszer @branan @offerijns -pallets/loans/* @mustermeiszer @branan @offerijns @lemunozm -pallets/interest-accrual/* @branan @lemunozm +pallets/pool-system/* @mustermeiszer @hieronx +pallets/loans/* @mustermeiszer @hieronx @lemunozm +pallets/interest-accrual/* @lemunozm pallets/investments/* @mustermeiszer pallets/permissions/* @mustermeiszer pallets/restricted-tokens/* @mustermeiszer pallets/data-collector/* @lemunozm -pallets/fees/* @lemunozm @mikiquantum -pallets/transfer-allowlist/* @thea-leake @mustermeiszer +pallets/fees/* @lemunozm +pallets/transfer-allowlist/* @mustermeiszer ## Changes to specific libraries libs/mock-builder/* @lemunozm @@ -41,9 +38,9 @@ libs/traits/src/changes.rs @lemunozm libs/traits/src/data.rs @lemunozm ## Changes to runtime -runtime/common/* @branan @mikiquantum @mustermeiszer @NunoAlexandre @offerijns @lemunozm -runtime/altair/* @branan @mikiquantum @mustermeiszer @NunoAlexandre @offerijns @wischli -runtime/centrifuge/* @branan @mikiquantum @mustermeiszer @NunoAlexandre @offerijns @wischli +runtime/common/* @mustermeiszer @NunoAlexandre @hieronx @lemunozm +runtime/altair/* @mustermeiszer @NunoAlexandre @hieronx @wischli +runtime/centrifuge/* @mustermeiszer @NunoAlexandre @hieronx @wischli ## Changes to integration tests runtime/integration-tests/* @mustermeiszer @NunoAlexandre @wischli @cdamian diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 620f64f578..e96cf1e6fb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,8 @@ # Description -Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. +[description] -Fixes # (issue) +Fixes #(issue) ## Changes and Descriptions