Fixed-Point Math
Full-precision x * y / denominator for i128 and I256 with explicit rounding
Overview
Besides the Wad decimal type, the math module exposes free functions that
compute x * y / denominator on i128 and I256 values with full precision and an explicit rounding direction.
They are the building blocks for share conversions, price scaling and other ratio computations where the
intermediate product may not fit the operand type.
Each operation comes in a panicking and a checked variant:
- Panicking variants panic with
SorobanFixedPointError::Overflow(1500) when the result overflows. - Checked variants (
checked_*) returnNoneinstead, for every failure including a zero denominator.
Functions
i128_fixed_point
pub fn mul_div_with_rounding(e: &Env, x: i128, y: i128, denominator: i128, rounding: Rounding) -> i128;
pub fn checked_mul_div_with_rounding(
e: &Env, x: i128, y: i128, denominator: i128, rounding: Rounding,
) -> Option<i128>;
pub fn mul_div(e: &Env, x: &i128, y: &i128, denominator: &i128) -> i128; // truncates toward zero
pub fn mul_div_floor(e: &Env, x: &i128, y: &i128, denominator: &i128) -> i128;
pub fn mul_div_ceil(e: &Env, x: &i128, y: &i128, denominator: &i128) -> i128;
pub fn checked_mul_div(e: &Env, x: &i128, y: &i128, denominator: &i128) -> Option<i128>;
pub fn checked_mul_div_floor(e: &Env, x: &i128, y: &i128, denominator: &i128) -> Option<i128>;
pub fn checked_mul_div_ceil(e: &Env, x: &i128, y: &i128, denominator: &i128) -> Option<i128>;i256_fixed_point
I256 carries its own Env, so these functions take no &Env.
pub fn mul_div_with_rounding(x: I256, y: I256, denominator: I256, rounding: Rounding) -> I256;
pub fn checked_mul_div_with_rounding(
x: I256, y: I256, denominator: I256, rounding: Rounding,
) -> Option<I256>;
pub fn mul_div(x: &I256, y: &I256, denominator: &I256) -> I256; // truncates toward zero
pub fn mul_div_floor(x: &I256, y: &I256, denominator: &I256) -> I256;
pub fn mul_div_ceil(x: &I256, y: &I256, denominator: &I256) -> I256;
pub fn checked_mul_div(x: &I256, y: &I256, denominator: &I256) -> Option<I256>;
pub fn checked_mul_div_floor(x: &I256, y: &I256, denominator: &I256) -> Option<I256>;
pub fn checked_mul_div_ceil(x: &I256, y: &I256, denominator: &I256) -> Option<I256>;In both modules, the *_with_rounding variants take their operands by value and the others by reference.
Rounding
#[contracttype]
pub enum Rounding {
Floor, // toward negative infinity
Ceil, // toward positive infinity
Truncate, // toward zero
}The direction matters whenever the result is not exact. Protocols typically round against the caller, e.g. a vault rounds shares minted on deposit down and assets required for a mint up.
Phantom Overflow
Computing x * y / denominator multiplies x by y first, and that intermediate product can be far larger than
the final result. A phantom overflow is when the intermediate product does not fit the type, even though the final
result does. For example, with i128, whose maximum is about 1.7 * 10^38:
x = 10^30 y = 10^30 denominator = 10^30
x * y = 10^60 // does not fit i128
x * y / denominator = 10^30 // fits i128A plain x * y / denominator overflows at x * y. The functions above compute the correct result instead:
-
i128: whenx * yoverflows, the calculation is retried withI256as the intermediate type, and the result is converted back toi128if it fits. -
I256: there is no wider type to fall back on. Instead, both operands are split into a multiple of the denominator plus a remainder, so that most of the division happens before the multiplication. This is an exact identity rather than an approximation:|x| = q1*D + r1 |y| = q2*D + r2 (D = |denominator|, 0 <= r1, r2 < D) floor(|x*y|/D) = q1*q2*D + q1*r2 + r1*q2 + floor(r1*r2/D)This holds for every
|denominator| <= 2^128(roughly3.4e38, far above the fixed-point scales in practical use:10^18,10^27,2^96,10^38). Within that domain the result is bit-for-bit what a 512-bit intermediate would produce. Above2^128the operation rejects rather than returning an incorrect value, and rejection is permitted rather than guaranteed: a large denominator whose remainders happen to be small still succeeds.
Phantom overflow handling covers all the free functions above and the checked_* methods of Wad. It does not
cover Wad's operators (+, -, *, /), which cannot reach an Env to build the intermediate; see the
Wad overflow bounds.
Errors
- The panicking variants raise
SorobanFixedPointError::Overflow(1500) when the result does not fit. - Domain errors (a zero
denominator,MIN / -1) are left to the platform rather than mapped to a contract error. Nativei128arithmetic surfaces as a generic wasm trap (Error(WasmVm, InvalidAction)), andI256host arithmetic asError(Object, ArithDomain). Inside theI256phantom overflow fallback, every failure, a zerodenominatorincluded, surfaces asOverflow. - The checked variants return
Nonefor all of the above.
A caller that needs a distinct division-by-zero error has to validate the denominator up front, as
Wad::from_ratio does with SorobanFixedPointError::DivisionByZero (1501).
Example
use soroban_sdk::{Env, I256};
use stellar_contract_utils::math::{i128_fixed_point, i256_fixed_point, Rounding};
// Share conversion, rounding in the vault's favour.
pub fn to_shares(e: &Env, assets: i128, total_shares: i128, total_assets: i128) -> i128 {
i128_fixed_point::mul_div_with_rounding(e, assets, total_shares, total_assets, Rounding::Floor)
}
// Assets required to mint `shares`, rounding up; `None` instead of panicking,
// e.g. when `total_shares` is zero.
pub fn assets_for_mint(e: &Env, shares: i128, total_assets: i128, total_shares: i128) -> Option<i128> {
i128_fixed_point::checked_mul_div_ceil(e, &shares, &total_assets, &total_shares)
}
// The same share conversion at 256-bit width.
pub fn to_shares_256(assets: &I256, total_shares: &I256, total_assets: &I256) -> I256 {
i256_fixed_point::mul_div_floor(assets, total_shares, total_assets)
}