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

Panic when process Lease::StateQuery #468

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion platform/packages/finance/src/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ use sdk::{
};

use crate::{
coin::Coin,
fraction::Fraction,
fractionable::{Fractionable, TimeSliceable},
fractionable::{CheckedMultiply, Fractionable, TimeSliceable},
ratio::Rational,
zero::Zero,
};
Expand Down Expand Up @@ -108,6 +109,31 @@ impl Duration {
{
Rational::new(amount, annual_amount).of(self)
}

/// Implementation note: This method uses the checked_mul method to safely perform the multiplication.
/// Returns None if the result exceeds the limits of the type.
pub fn into_slice_per_ratio_checked<U>(self, amount: U, annual_amount: U) -> Option<Self>
where
Self: Fractionable<U> + CheckedMultiply<U>,
U: Zero + Debug + PartialEq + Copy,
{
self.checked_mul(amount, annual_amount)
}
}

impl<C> CheckedMultiply<Coin<C>> for Duration {
#[track_caller]
fn checked_mul(self, parts: Coin<C>, total: Coin<C>) -> Option<Self>
where
Coin<C>: Zero + Debug + PartialEq<Coin<C>>,
Self: Sized,
{
let d128: u128 = self.into();

CheckedMultiply::<Coin<C>>::checked_mul(d128, parts, total)
.and_then(|res| res.try_into().ok())
.map(Self::from_nanos)
}
}

impl From<Duration> for u128 {
Expand Down
46 changes: 34 additions & 12 deletions platform/packages/finance/src/fractionable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,53 @@ pub trait HigherRank<T> {
type Intermediate;
}

impl<T, D, DIntermediate, U> Fractionable<U> for T
pub trait CheckedMultiply<U> {
#[track_caller]
fn checked_mul(self, parts: U, total: U) -> Option<Self>
where
U: Zero + Debug + PartialEq<U>,
Self: Sized;
}

impl<T, D, DIntermediate, U> CheckedMultiply<U> for T
where
T: HigherRank<U, Type = D, Intermediate = DIntermediate> + Into<D>,
D: TryInto<DIntermediate>,
DIntermediate: Into<T>,
D: Mul<D, Output = D> + Div<D, Output = D>,
U: Zero + PartialEq + Into<D> + Debug,
{
fn checked_mul(self, parts: U, total: U) -> Option<Self> {
if parts == total {
Some(self)
} else {
let res_double: D = self.into() * parts.into();
let res_double = res_double / total.into();
res_double
.try_into()
.ok()
.map(|res_intermediate: DIntermediate| res_intermediate.into())
}
}
}

impl<T, D, DIntermediate, U> Fractionable<U> for T
where
T: HigherRank<U, Type = D, Intermediate = DIntermediate> + Into<D> + CheckedMultiply<U>,
D: TryInto<DIntermediate>,
<D as TryInto<DIntermediate>>::Error: Debug,
DIntermediate: Into<T>,
D: Mul<D, Output = D> + Div<D, Output = D>,
U: Zero + PartialEq + Into<D>,
U: Zero + PartialEq + Into<D> + Debug,
{
#[track_caller]
fn safe_mul<R>(self, ratio: &R) -> Self
where
R: Ratio<U>,
{
// TODO debug_assert_eq!(T::BITS * 2, D::BITS);

if ratio.parts() == ratio.total() {
self
} else {
let res_double: D = self.into() * ratio.parts().into();
let res_double = res_double / ratio.total().into();
let res_intermediate: DIntermediate =
res_double.try_into().expect("unexpected overflow");
res_intermediate.into()
}
self.checked_mul(ratio.parts(), ratio.total())
.expect("unexpected overflow")
}
}

Expand Down
34 changes: 28 additions & 6 deletions protocol/contracts/lease/src/lease/due.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ impl DueTrait for State {
self.principal_due,
Duration::YEAR,
);
if total_interest_a_year.is_zero() {
Duration::MAX
} else {
Duration::YEAR.into_slice_per_ratio(overdue_left, total_interest_a_year)
}

// FIX for PR#370
Duration::YEAR
.into_slice_per_ratio_checked(overdue_left, total_interest_a_year)
.map_or(Duration::MAX, |time_to_accrue_min_amount| {
time_to_accrue_min_amount
})
};
let time_to_collect = self.overdue.start_in().max(time_to_accrue_min_amount);
if time_to_collect == Duration::default() {
Expand All @@ -53,7 +55,7 @@ mod test {

use crate::{
loan::{Overdue, State},
position::DueTrait,
position::{DueTrait, OverdueCollection},
};

#[test]
Expand Down Expand Up @@ -230,4 +232,24 @@ mod test {
assert_eq!(Coin::ZERO, overdue_collection.amount());
assert_eq!(principal_due + total_interest, s.total_due());
}

#[test]
fn test_large_interest_accrual_period() {
let principal_due = 20.into();
let due_interest = 5.into();
let due_margin_interest = 1.into();
let till_due_end = Duration::from_days(1);
let s = State {
annual_interest: Percent::from_percent(15),
annual_interest_margin: Percent::from_percent(0),
principal_due,
due_interest,
due_margin_interest,
overdue: Overdue::StartIn(till_due_end),
};
assert_eq!(
OverdueCollection::StartIn(Duration::MAX),
s.overdue_collection(1_800.into())
);
}
}
2 changes: 1 addition & 1 deletion protocol/contracts/lease/src/position/interest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub trait Due {
/// When overdue interest amount goes above a configured minimum then the interest becomes collectable.
fn overdue_collection(&self, min_amount: LpnCoin) -> OverdueCollection;
}

#[derive(PartialEq, Debug)]
pub enum OverdueCollection {
/// No collectable overdue interest yet
///
Expand Down
Loading