Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loans: add slippage for external pricing #1487

Merged
merged 14 commits into from
Aug 16, 2023
2 changes: 1 addition & 1 deletion pallets/loans/docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ package pricing {
price_id: PriceId,
max_borrow_quantity: MaxBorrowAmount,
notional: Balance,
pool_id: PoolId
max_variation_price: Perthing,
}

ExternalPricing *-l-> MaxBorrowAmount
Expand Down
51 changes: 23 additions & 28 deletions pallets/loans/src/entities/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,15 @@ impl<T: Config> ActiveLoan<T> {
self.write_down(value)
}

fn ensure_can_borrow(&self, amount: &PricingAmount<T>) -> DispatchResult {
fn ensure_can_borrow(&self, amount: &PricingAmount<T>, pool_id: T::PoolId) -> DispatchResult {
let max_borrow_amount = match &self.pricing {
ActivePricing::Internal(inner) => {
amount.internal()?;
inner.max_borrow_amount(self.total_borrowed)?
}
ActivePricing::External(inner) => {
let external_amount = amount.external()?;
inner.max_borrow_amount(external_amount)?
inner.max_borrow_amount(external_amount, pool_id)?
}
};

Expand Down Expand Up @@ -317,8 +317,8 @@ impl<T: Config> ActiveLoan<T> {
Ok(())
}

pub fn borrow(&mut self, amount: &PricingAmount<T>) -> DispatchResult {
self.ensure_can_borrow(amount)?;
pub fn borrow(&mut self, amount: &PricingAmount<T>, pool_id: T::PoolId) -> DispatchResult {
self.ensure_can_borrow(amount, pool_id)?;

self.total_borrowed.ensure_add_assign(amount.balance()?)?;

Expand All @@ -344,6 +344,7 @@ impl<T: Config> ActiveLoan<T> {
fn prepare_repayment(
&self,
mut amount: RepaidPricingAmount<T>,
pool_id: T::PoolId,
) -> Result<RepaidPricingAmount<T>, DispatchError> {
let (max_repay_principal, outstanding_interest) = match &self.pricing {
ActivePricing::Internal(inner) => {
Expand All @@ -357,7 +358,7 @@ impl<T: Config> ActiveLoan<T> {
}
ActivePricing::External(inner) => {
let external_amount = amount.principal.external()?;
let max_repay_principal = inner.max_repay_principal(external_amount)?;
let max_repay_principal = inner.max_repay_principal(external_amount, pool_id)?;

(max_repay_principal, inner.outstanding_interest()?)
}
Expand Down Expand Up @@ -387,8 +388,9 @@ impl<T: Config> ActiveLoan<T> {
pub fn repay(
&mut self,
amount: RepaidPricingAmount<T>,
pool_id: T::PoolId,
) -> Result<RepaidPricingAmount<T>, DispatchError> {
let amount = self.prepare_repayment(amount)?;
let amount = self.prepare_repayment(amount, pool_id)?;

self.total_repaid
.ensure_add_assign(&amount.repaid_amount()?)?;
Expand Down Expand Up @@ -512,32 +514,25 @@ impl<T: Config> TryFrom<(T::PoolId, ActiveLoan<T>)> for ActiveLoanInfo<T> {
type Error = DispatchError;

fn try_from((pool_id, active_loan): (T::PoolId, ActiveLoan<T>)) -> Result<Self, Self::Error> {
let (present_value, outstanding_principal, outstanding_interest) =
match &active_loan.pricing {
ActivePricing::Internal(inner) => {
let principal = active_loan
.total_borrowed
.ensure_sub(active_loan.total_repaid.principal)?;
let maturity_date = active_loan.schedule.maturity.date();

(
inner.present_value(active_loan.origination_date, maturity_date)?,
principal,
inner.outstanding_interest(principal)?,
)
}
ActivePricing::External(inner) => (
inner.present_value(pool_id)?,
inner.outstanding_principal(pool_id)?,
inner.outstanding_interest()?,
),
};
let (outstanding_principal, outstanding_interest) = match &active_loan.pricing {
ActivePricing::Internal(inner) => {
let principal = active_loan
.total_borrowed
.ensure_sub(active_loan.total_repaid.principal)?;

(principal, inner.outstanding_interest(principal)?)
}
ActivePricing::External(inner) => (
inner.outstanding_principal(pool_id)?,
inner.outstanding_interest()?,
),
};

Ok(Self {
active_loan,
present_value,
present_value: active_loan.present_value(pool_id)?,
outstanding_principal,
outstanding_interest,
active_loan,
})
}
}
38 changes: 36 additions & 2 deletions pallets/loans/src/entities/pricing/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{self, ensure, RuntimeDebug, RuntimeDebugNoBound};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{EnsureAdd, EnsureFixedPointNumber, EnsureSub, Zero},
ArithmeticError, DispatchError, DispatchResult, FixedPointNumber,
traits::{EnsureAdd, EnsureFixedPointNumber, EnsureInto, EnsureSub, Zero},
ArithmeticError, DispatchError, DispatchResult, FixedPointNumber, PerThing,
};

use crate::{
Expand Down Expand Up @@ -67,6 +67,11 @@ pub struct ExternalPricing<T: Config> {

/// Reference price used to calculate the interest
pub notional: T::Balance,

/// Maximum variation between the settlement price chosen for
/// borrow/repay and the current oracle price.
/// Represented as: Oracle price +/- oracle price * max_variation_price
lemunozm marked this conversation as resolved.
Show resolved Hide resolved
pub max_variation_price: T::PerThing,
lemunozm marked this conversation as resolved.
Show resolved Hide resolved
}

impl<T: Config> ExternalPricing<T> {
Expand Down Expand Up @@ -148,10 +153,36 @@ impl<T: Config> ExternalActivePricing<T> {
Ok(self.outstanding_quantity.ensure_mul_int(price)?)
}

fn validate_amount(
&self,
amount: &ExternalAmount<T>,
pool_id: T::PoolId,
) -> Result<(), DispatchError> {
let price = T::PriceRegistry::get(&self.info.price_id, &pool_id)?.0;
let delta = if amount.settlement_price > price {
amount.settlement_price.ensure_sub(price)?
} else {
price.ensure_sub(amount.settlement_price)?
};
let variation = T::PerThing::from_rational(delta.ensure_into()?, price.ensure_into()?);
lemunozm marked this conversation as resolved.
Show resolved Hide resolved

// We bypass any price if quantity is zero,
// because it does not take effect in the computation.
ensure!(
variation <= self.info.max_variation_price || amount.quantity.is_zero(),
Error::<T>::SettlementPriceExceedsVariation
);

Ok(())
}

pub fn max_borrow_amount(
&self,
amount: ExternalAmount<T>,
pool_id: T::PoolId,
) -> Result<T::Balance, DispatchError> {
self.validate_amount(&amount, pool_id)?;

match self.info.max_borrow_amount {
MaxBorrowAmount::Quantity(quantity) => {
let available = quantity.ensure_sub(self.outstanding_quantity)?;
Expand All @@ -164,7 +195,10 @@ impl<T: Config> ExternalActivePricing<T> {
pub fn max_repay_principal(
&self,
amount: ExternalAmount<T>,
pool_id: T::PoolId,
) -> Result<T::Balance, DispatchError> {
self.validate_amount(&amount, pool_id)?;

Ok(self
.outstanding_quantity
.ensure_mul_int(amount.settlement_price)?)
Expand Down
15 changes: 10 additions & 5 deletions pallets/loans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ pub mod pallet {
};
use frame_system::pallet_prelude::*;
use scale_info::TypeInfo;
use sp_arithmetic::FixedPointNumber;
use sp_arithmetic::{FixedPointNumber, PerThing};
use sp_runtime::{
traits::{BadOrigin, EnsureAdd, EnsureAddAssign, EnsureInto, One, Zero},
ArithmeticError, FixedPointOperand, TransactionOutcome,
Expand Down Expand Up @@ -161,9 +161,12 @@ pub mod pallet {
/// Defines the balance type used for math computations
type Balance: tokens::Balance + FixedPointOperand;

/// Type to represent different quantities in external pricing.
/// Type to represent different quantities
type Quantity: Parameter + Member + FixedPointNumber + TypeInfo + MaxEncodedLen;

/// Defines the perthing type used where values can not overpass 100%
type PerThing: Parameter + Member + PerThing + TypeInfo + MaxEncodedLen;

/// Fetching method for the time of the current block
type Time: UnixTime;

Expand Down Expand Up @@ -365,6 +368,8 @@ pub mod pallet {
UnrelatedChangeId,
/// Emits when the pricing method is not compatible with the input
MismatchedPricingMethod,
/// Emits when settlement price is exceeds the configured variation.
SettlementPriceExceedsVariation,
/// Emits when the loan is incorrectly specified and can not be created
CreateLoanError(CreateLoanError),
/// Emits when the loan can not be borrowed from
Expand Down Expand Up @@ -473,14 +478,14 @@ pub mod pallet {
Self::ensure_loan_borrower(&who, created_loan.borrower())?;

let mut active_loan = created_loan.activate(pool_id)?;
active_loan.borrow(&amount)?;
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)
loan.borrow(&amount, pool_id)
})?
.1
}
Expand Down Expand Up @@ -519,7 +524,7 @@ pub mod pallet {

let (amount, _count) = Self::update_active_loan(pool_id, loan_id, |loan| {
Self::ensure_loan_borrower(&who, loan.borrower())?;
loan.repay(amount.clone())
loan.repay(amount, pool_id)
})?;

T::Pool::deposit(pool_id, who, amount.repaid_amount()?.total()?)?;
Expand Down
69 changes: 60 additions & 9 deletions pallets/loans/src/tests/borrow_loan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,19 +327,41 @@ fn with_wrong_quantity_amount_external_pricing() {
}

#[test]
fn with_correct_amount_external_pricing() {
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);
// Higher
let amount = ExternalAmount::new(
QUANTITY,
PRICE_VALUE + (MAX_VARIATION_PRICE.mul_floor(PRICE_VALUE) + 1),
);
config_mocks(amount.balance().unwrap());
assert_noop!(
Loans::borrow(
RuntimeOrigin::signed(BORROWER),
POOL_A,
loan_id,
PricingAmount::External(amount)
),
Error::<Runtime>::SettlementPriceExceedsVariation
);

assert_ok!(Loans::borrow(
RuntimeOrigin::signed(BORROWER),
POOL_A,
loan_id,
PricingAmount::External(amount)
));
// Lower
let amount = ExternalAmount::new(
QUANTITY,
PRICE_VALUE - (MAX_VARIATION_PRICE.mul_floor(PRICE_VALUE) + 1),
);
config_mocks(amount.balance().unwrap());
assert_noop!(
Loans::borrow(
RuntimeOrigin::signed(BORROWER),
POOL_A,
loan_id,
PricingAmount::External(amount)
),
Error::<Runtime>::SettlementPriceExceedsVariation
);
});
}

Expand All @@ -348,7 +370,36 @@ 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 * 2 /* Any value */);
// Higher
let amount = ExternalAmount::new(
QUANTITY / 3.into(),
PRICE_VALUE + MAX_VARIATION_PRICE.mul_floor(PRICE_VALUE),
);
config_mocks(amount.balance().unwrap());

assert_ok!(Loans::borrow(
RuntimeOrigin::signed(BORROWER),
POOL_A,
loan_id,
PricingAmount::External(amount)
));

// Same
let amount = ExternalAmount::new(QUANTITY / 3.into(), PRICE_VALUE);
config_mocks(amount.balance().unwrap());

assert_ok!(Loans::borrow(
RuntimeOrigin::signed(BORROWER),
POOL_A,
loan_id,
PricingAmount::External(amount)
));

// Lower
let amount = ExternalAmount::new(
QUANTITY / 3.into(),
PRICE_VALUE - MAX_VARIATION_PRICE.mul_floor(PRICE_VALUE),
);
config_mocks(amount.balance().unwrap());

assert_ok!(Loans::borrow(
Expand Down
6 changes: 4 additions & 2 deletions pallets/loans/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use frame_support::traits::{
};
use frame_system::{EnsureRoot, EnsureSigned};
use scale_info::TypeInfo;
use sp_arithmetic::fixed_point::FixedU64;
use sp_arithmetic::{fixed_point::FixedU64, Perbill};
use sp_core::H256;
use sp_runtime::{
testing::Header,
Expand Down Expand Up @@ -70,8 +70,9 @@ pub const REGISTER_PRICE_ID: PriceId = 42;
pub const UNREGISTER_PRICE_ID: PriceId = 88;
pub const PRICE_VALUE: Balance = 998;
pub const NOTIONAL: Balance = 1000;
pub const QUANTITY: Quantity = Quantity::from_rational(20, 1);
pub const QUANTITY: Quantity = Quantity::from_rational(12, 1);
pub const CHANGE_ID: ChangeId = H256::repeat_byte(0x42);
pub const MAX_VARIATION_PRICE: Perbill = Perbill::from_percent(1);

type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Runtime>;
type Block = frame_system::mocking::MockBlock<Runtime>;
Expand Down Expand Up @@ -230,6 +231,7 @@ impl pallet_loans::Config for Runtime {
type MaxActiveLoansPerPool = MaxActiveLoansPerPool;
type MaxWriteOffPolicySize = MaxWriteOffPolicySize;
type NonFungible = Uniques;
type PerThing = Perbill;
type Permissions = MockPermissions;
type Pool = MockPools;
type PoolId = PoolId;
Expand Down