Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions framework/base/src/types/managed/wrapped/decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod decimals;
mod managed_decimal;
mod managed_decimal_cmp;
mod managed_decimal_cmp_signed;
mod managed_decimal_half_up;
mod managed_decimal_logarithm;
mod managed_decimal_op_add;
mod managed_decimal_op_add_signed;
Expand Down
8 changes: 5 additions & 3 deletions framework/base/src/types/managed/wrapped/decimal/decimals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
pub type NumDecimals = usize;

/// Implemented by all decimal types usable in `ManagedDecimal`.
pub trait Decimals {
pub trait Decimals: Clone {
/// Number of decimals as variable.
fn num_decimals(&self) -> NumDecimals;

Expand Down Expand Up @@ -46,8 +46,10 @@ pub type LnDecimals = ConstDecimals<U9>;
pub type EgldDecimals = ConstDecimals<U18>;

impl<DECIMALS: Unsigned> ConstDecimals<DECIMALS> {
pub fn new() -> Self {
Self::default()
pub const fn new() -> Self {
ConstDecimals {
_phantom: PhantomData,
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ impl<M: ManagedTypeApi, D: Decimals> ManagedDecimal<M, D> {
ManagedDecimal { data, decimals }
}

/// Returns the multiplicative identity `1` at the given `decimals` precision.
///
/// The raw value is `10^decimals` (the scaling factor), so that
/// `self.trunc()` returns `1` and all arithmetic treats it as unity.
pub fn one(decimals: D) -> Self {
let data = (*decimals.scaling_factor::<M>()).clone();
ManagedDecimal { data, decimals }
}

pub fn scale(&self) -> usize {
self.decimals.num_decimals()
}
Expand Down Expand Up @@ -123,7 +132,7 @@ impl<M: ManagedTypeApi, DECIMALS: Unsigned> From<ManagedDecimal<M, ConstDecimals
}
}

impl<M: ManagedTypeApi, D: Decimals + Clone> ManagedDecimal<M, D> {
impl<M: ManagedTypeApi, D: Decimals> ManagedDecimal<M, D> {
/// Integer part of the k-th root, preserving the decimal scale.
///
/// Internally pre-scales the raw data by `scaling_factor^(k-1)` so that after
Expand Down Expand Up @@ -154,6 +163,91 @@ impl<M: ManagedTypeApi, D: Decimals + Clone> ManagedDecimal<M, D> {
let scaled = &self.data * &sf.pow(k.saturating_sub(1));
ManagedDecimal::from_raw_units(scaled.nth_root_unchecked(k), self.decimals.clone())
}

/// Approximates e^`self` using a 5-term Taylor series.
///
/// Treats `self` as the exponent `x` and computes:
///
/// ```text
/// e^x ≈ 1 + x + x²/2! + x³/3! + x⁴/4! + x⁵/5!
/// ```
///
/// The result has the same precision as `self`; all intermediate steps use
/// [`mul_half_up`] / [`div_half_up`] to prevent rounding errors from
/// accumulating toward zero.
///
/// Accurate for small `x` (i.e. when `x ≪ 1`). Error is O(x⁶/720).
pub fn exp_approx(&self) -> ManagedDecimal<M, D>
where
ManagedDecimal<M, D>: core::ops::Add<Output = ManagedDecimal<M, D>>,
{
let one = ManagedDecimal::<M, D>::one(self.decimals.clone());

// Higher powers of x (x = self)
let x_sq = self.mul_half_up(self, self.decimals.clone());
let x_cub = x_sq.mul_half_up(self, self.decimals.clone());
let x_pow4 = x_cub.mul_half_up(self, self.decimals.clone());
let x_pow5 = x_pow4.mul_half_up(self, self.decimals.clone());

// x^n / n! — reuse one ManagedDecimal, overwriting its data for each factorial
const FACT_2: u64 = 2;
const FACT_3: u64 = 6;
const FACT_4: u64 = 24;
const FACT_5: u64 = 120;
let mut factor =
ManagedDecimal::<M, NumDecimals>::from_raw_units(BigUint::from(FACT_2), 0usize);
let term2 = x_sq.div_half_up(&factor, self.decimals.clone());
factor.data.overwrite_u64(FACT_3);
let term3 = x_cub.div_half_up(&factor, self.decimals.clone());
factor.data.overwrite_u64(FACT_4);
let term4 = x_pow4.div_half_up(&factor, self.decimals.clone());
factor.data.overwrite_u64(FACT_5);
let term5 = x_pow5.div_half_up(&factor, self.decimals.clone());

// 1 + x + x²/2! + x³/3! + x⁴/4! + x⁵/5!
let mut result = one;
result += self; // using += allows us to avoid cloning self
result += term2;
result += term3;
result += term4;
result += term5;
result
}

/// Computes the continuous-compounding growth factor e^(`self` × `expiration`).
///
/// Delegates to [`exp_approx`] after computing `x = rate * expiration`;
/// uses a 5-term Taylor series internally:
///
/// ```text
/// e^(rate * t) ≈ 1 + x + x²/2! + x³/3! + x⁴/4! + x⁵/5!, where x = rate * t
/// ```
///
/// Multiply a principal amount by the returned factor to apply interest.
/// Returns `1` (at `precision`) when `expiration == 0`.
///
/// # Credits
/// Original implementation by [@mihaieremia](https://github.com/mihaieremia).
pub fn compounded_interest_factor<Precision: Decimals>(
&self,
expiration: u64,
precision: Precision,
) -> ManagedDecimal<M, Precision>
where
ManagedDecimal<M, Precision>: core::ops::Add<Output = ManagedDecimal<M, Precision>>,
{
if expiration == 0 {
return ManagedDecimal::<M, Precision>::one(precision.clone());
}

// Represent the time delta as an exact integer decimal (0 dp)
let expiration_decimal =
ManagedDecimal::<M, NumDecimals>::from_raw_units(BigUint::from(expiration), 0usize);

// x = rate * time_delta
let x = self.mul_half_up(&expiration_decimal, precision.clone());
x.exp_approx()
}
}

impl<M: ManagedTypeApi> ManagedVecItem for ManagedDecimal<M, NumDecimals> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use crate::{api::ManagedTypeApi, types::Sign};

use super::{Decimals, ManagedDecimal, ManagedDecimalSigned};

impl<M: ManagedTypeApi, D1: Decimals> ManagedDecimal<M, D1> {
/// Multiplies two decimals with half-up rounding to a target precision.
///
/// Both operands are first rescaled to `precision`. The rescaled raw values
/// are multiplied, producing a result with `2 * precision` implied decimal
/// places. That intermediate value is then rounded back to `precision` using
/// the standard pre-bias trick:
///
/// ```text
/// rounded = (product + scale / 2) / scale
/// ```
///
/// Adding `scale / 2` before the integer division means that any remainder
/// ≥ half the scale (i.e. the fractional part ≥ 0.5) causes the quotient
/// to increment by one — equivalent to round-half-up.
///
/// # Credits
/// Original implementation by [@mihaieremia](https://github.com/mihaieremia).
pub fn mul_half_up<D2: Decimals, DResult: Decimals>(
&self,
other: &ManagedDecimal<M, D2>,
precision: DResult,
) -> ManagedDecimal<M, DResult> {
// Use target precision directly, no +1
let scaled_a = self.rescale(precision.clone());
let scaled_b = other.rescale(precision.clone());

// Perform multiplication in BigUint
let product = scaled_a.data * scaled_b.data;

// Half-up rounding at precision
let scale = precision.scaling_factor();
let half_scaled = &*scale / 2u64;

// Round half-up
let rounded_product = (product + half_scaled) / &*scale;

ManagedDecimal::from_raw_units(rounded_product, precision)
}

/// Divides two decimals with half-up rounding to a target precision.
///
/// Both operands are rescaled to `precision`. The numerator is then
/// multiplied by `scale` so the division produces a result with the correct
/// number of decimal places. The quotient is rounded using the pre-bias
/// trick:
///
/// ```text
/// rounded = (numerator * scale + denominator / 2) / denominator
/// ```
///
/// Adding `denominator / 2` means that once the true quotient's remainder
/// reaches half the denominator (i.e. fractional part ≥ 0.5), integer
/// division increments the result — equivalent to round-half-up.
///
/// # Credits
/// Original implementation by [@mihaieremia](https://github.com/mihaieremia).
pub fn div_half_up<D2: Decimals, DResult: Decimals>(
&self,
other: &ManagedDecimal<M, D2>,
precision: DResult,
) -> ManagedDecimal<M, DResult> {
let scaled_a = self.rescale(precision.clone());
let scaled_b = other.rescale(precision.clone());

// Perform division in BigUint
let scale = precision.scaling_factor();
let numerator = scaled_a.into_raw_units() * &*scale;
let denominator = scaled_b.into_raw_units();

// Half-up rounding
let half_denominator = denominator.clone() / 2u64;
let rounded_quotient = (numerator + half_denominator) / denominator;

ManagedDecimal::from_raw_units(rounded_quotient, precision)
}
}

impl<M: ManagedTypeApi, D1: Decimals> ManagedDecimalSigned<M, D1> {
/// Multiplies two signed decimals with half-up (away-from-zero) rounding
/// to a target precision.
///
/// The algorithm mirrors [`ManagedDecimal::mul_half_up`], but uses `BigInt`
/// arithmetic and adjusts the pre-bias direction based on the sign of the
/// intermediate product:
///
/// ```text
/// if product < 0: rounded = (product - scale / 2) / scale
/// else: rounded = (product + scale / 2) / scale
/// ```
///
/// The VM's integer division truncates toward zero. Subtracting the bias
/// for a negative product pushes it *further* from zero before truncation,
/// so the final result rounds away from zero in both directions — matching
/// the conventional financial definition of "round half up" for signed
/// numbers.
///
/// # Credits
/// Original implementation by [@mihaieremia](https://github.com/mihaieremia).
pub fn mul_half_up_signed<D2: Decimals, DResult: Decimals>(
&self,
other: &ManagedDecimalSigned<M, D2>,
precision: DResult,
) -> ManagedDecimalSigned<M, DResult> {
let scaled_a = self.rescale(precision.clone());
let scaled_b = other.rescale(precision.clone());

// Perform multiplication in BigInt
let product = scaled_a.data * scaled_b.data;

// Half-up rounding at precision
let scale = precision.scaling_factor();
let half_scaled = (scale.clone() / 2u64).into_big_int();

// Sign-aware "away-from-zero" rounding
let rounded_product = if product.sign() == Sign::Minus {
(product - half_scaled) / scale.as_big_int()
} else {
(product + half_scaled) / scale.as_big_int()
};

ManagedDecimalSigned::from_raw_units(rounded_product, precision)
}

/// Divides two signed decimals with half-up (away-from-zero) rounding
/// to a target precision.
///
/// The numerator is scaled up by `scale` (as in [`ManagedDecimal::div_half_up`])
/// and then pre-biased before T-division (truncates toward zero). The bias
/// direction depends solely on the sign of the numerator — **not** on the
/// sign of the denominator:
///
/// ```text
/// half = |denominator| / 2
/// if numerator < 0: rounded = (numerator - half) / denominator
/// else: rounded = (numerator + half) / denominator
/// ```
///
/// This is correct because `half` is always non-negative. When `numerator > 0`,
/// adding `half` increases the numerator's magnitude; when the denominator is
/// negative, dividing a larger positive numerator yields a more-negative
/// result — farther from zero. The rule therefore rounds away from zero for
/// all four sign combinations of `(numerator, denominator)`.
///
/// Using `sign(denominator)` as the branch condition instead would produce
/// wrong results whenever the denominator is negative.
///
/// # Credits
/// Original implementation by [@mihaieremia](https://github.com/mihaieremia).
pub fn div_half_up_signed<D2: Decimals, DResult: Decimals>(
&self,
other: &ManagedDecimalSigned<M, D2>,
precision: DResult,
) -> ManagedDecimalSigned<M, DResult> {
let scaled_a = self.rescale(precision.clone());
let scaled_b = other.rescale(precision.clone());

let scale = precision.scaling_factor();
let numerator = scaled_a.data * scale.as_big_int();
let denominator = scaled_b.data;

// Half-up rounding
let half_denominator = (denominator.magnitude() / 2u64).into_big_int();
let rounded_quotient = if numerator.sign() == Sign::Minus {
(numerator - half_denominator) / denominator
} else {
(numerator + half_denominator) / denominator
};

ManagedDecimalSigned::from_raw_units(rounded_quotient, precision)
}
}
Loading
Loading