diff --git a/pallets/loans/src/lib.rs b/pallets/loans/src/lib.rs index 8b592b2eab..e79832ed7b 100644 --- a/pallets/loans/src/lib.rs +++ b/pallets/loans/src/lib.rs @@ -355,6 +355,12 @@ pub mod pallet { loan_id: T::LoanId, amount: PrincipalInput, }, + /// Debt of a loan has been decreased + DebtDecreased { + pool_id: T::PoolId, + loan_id: T::LoanId, + amount: RepaidInput, + }, } #[pallet::error] @@ -890,6 +896,34 @@ pub mod pallet { Ok(()) } + + /// Decrease debt for a loan. Similar to [`Pallet::repay()`] but + /// without transferring from the pool. + /// + /// The origin must be the borrower of the loan. + /// The decrease debt action should fulfill the repay restrictions + /// configured at [`types::LoanRestrictions`]. The portfolio valuation + /// of the pool is updated to reflect the new present value of the loan. + #[pallet::weight(T::WeightInfo::increase_debt(T::MaxActiveLoansPerPool::get()))] + #[pallet::call_index(14)] + pub fn decrease_debt( + origin: OriginFor, + pool_id: T::PoolId, + loan_id: T::LoanId, + amount: RepaidInput, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let (amount, _count) = Self::repay_action(&who, pool_id, loan_id, &amount, false)?; + + Self::deposit_event(Event::::DebtDecreased { + pool_id, + loan_id, + amount, + }); + + Ok(()) + } } // Loan actions diff --git a/pallets/loans/src/tests/borrow_loan.rs b/pallets/loans/src/tests/borrow_loan.rs index 20f51c8ae3..c74281bb65 100644 --- a/pallets/loans/src/tests/borrow_loan.rs +++ b/pallets/loans/src/tests/borrow_loan.rs @@ -601,9 +601,22 @@ fn increase_debt_does_not_withdraw() { let loan_id = util::create_loan(loan); let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); - config_mocks(amount.balance().unwrap()); + MockPrices::mock_get(|id, pool_id| { + assert_eq!(*pool_id, POOL_A); + match *id { + REGISTER_PRICE_ID => Ok((PRICE_VALUE, BLOCK_TIME_MS)), + _ => Err(PRICE_ID_NO_FOUND), + } + }); + MockPrices::mock_register_id(|id, pool_id| { + assert_eq!(*pool_id, POOL_A); + match *id { + REGISTER_PRICE_ID => Ok(()), + _ => Err(PRICE_ID_NO_FOUND), + } + }); - assert_ok!(Loans::borrow( + assert_ok!(Loans::increase_debt( RuntimeOrigin::signed(BORROWER), POOL_A, loan_id, diff --git a/pallets/loans/src/tests/repay_loan.rs b/pallets/loans/src/tests/repay_loan.rs index 87d88ba7c2..7772b14849 100644 --- a/pallets/loans/src/tests/repay_loan.rs +++ b/pallets/loans/src/tests/repay_loan.rs @@ -978,3 +978,44 @@ fn with_external_pricing() { assert_eq!(current_price(), NOTIONAL); }); } + +#[test] +fn decrease_debt_does_not_deposit() { + new_test_ext().execute_with(|| { + MockPools::mock_deposit(|_, _, _| { + unreachable!("increase debt must not withdraw funds from the pool"); + }); + + let loan = LoanInfo { + pricing: Pricing::External(ExternalPricing { + max_borrow_amount: ExtMaxBorrowAmount::NoLimit, + ..util::base_external_pricing() + }), + ..util::base_external_loan() + }; + + let loan_id = util::create_loan(loan); + + let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE); + util::borrow_loan(loan_id, PrincipalInput::External(amount.clone())); + + MockPrices::mock_get(move |id, pool_id| { + assert_eq!(*pool_id, POOL_A); + match *id { + REGISTER_PRICE_ID => Ok((PRICE_VALUE, BLOCK_TIME_MS)), + _ => Err(PRICE_ID_NO_FOUND), + } + }); + + assert_ok!(Loans::decrease_debt( + RuntimeOrigin::signed(BORROWER), + POOL_A, + loan_id, + RepaidInput { + principal: PrincipalInput::External(amount), + interest: 0, + unscheduled: 0, + } + )); + }); +}