Skip to content
Merged
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
20 changes: 13 additions & 7 deletions math/core/sources/decimal_scaling.move
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const MAX_DECIMALS: u8 = 24;
///
/// #### Examples
///
/// ```
/// ```move
/// // Scaling down: Ethereum to Sui (precision preserved for clean values)
/// // 1.0 token with 18 decimals = 1000000000000000000
/// // 1.0 token with 9 decimals = 1000000000
Expand Down Expand Up @@ -132,7 +132,7 @@ public fun safe_downcast_balance(raw_amount: u256, source_decimals: u8, target_d
///
/// #### Examples
///
/// ```
/// ```move
/// // Scaling up: Sui to Ethereum (no precision loss)
/// // 1.0 token with 9 decimals = 1000000000
/// // 1.0 token with 18 decimals = 1000000000000000000
Expand All @@ -155,7 +155,7 @@ public fun safe_upcast_balance(amount: u64, source_decimals: u8, target_decimals

/// Internal helper to scale an amount between different decimal precisions.
///
/// # Truncation Behavior
/// #### Truncation Behavior
///
/// When scaling down (source_decimals > target_decimals), this function uses
/// integer division which TRUNCATES the result. Fractional parts are discarded,
Expand All @@ -177,10 +177,16 @@ public fun safe_upcast_balance(amount: u64, source_decimals: u8, target_decimals
///
/// #### Examples
///
/// - Scaling up: amount=1000000, source=6, target=9 → 1000000000 (no precision loss).
/// - Scaling down: amount=1000000000, source=9, target=6 → 1000000.
/// - Same decimals: amount=1000000000, source=9, target=9 → 1000000000 (no conversion).
/// - Truncation example: amount=1999000000, source=9, target=0 → 1 (not 2).
/// ```move
/// // Scaling up (no precision loss): 1000000 at 6 decimals -> 1000000000 at 9 decimals.
/// let up = scale_amount(1000000, 6, 9);
/// // Scaling down: 1000000000 at 9 decimals -> 1000000 at 6 decimals.
/// let down = scale_amount(1000000000, 9, 6);
/// // Same decimals: no conversion.
/// let same = scale_amount(1000000000, 9, 9);
/// // Truncation: 1999000000 at 9 decimals -> 1 at 0 decimals (not 2).
/// let truncated = scale_amount(1999000000, 9, 0);
/// ```
fun scale_amount(amount: u256, source_decimals: u8, target_decimals: u8): u256 {
// Fast path: same decimals, no scaling needed.
if (source_decimals == target_decimals) {
Expand Down
68 changes: 46 additions & 22 deletions math/core/sources/internal/macros.move
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,28 @@ use openzeppelin_math::u512;

// === Errors ===

/// Raised when a division or modular operation is given a zero divisor.
#[error(code = 0)]
const EDivideByZero: vector<u8> = "Divisor must be non-zero";
/// Raised when a modular operation is given a zero modulus.
#[error(code = 1)]
const EZeroModulus: vector<u8> = "Modulus must be non-zero";

// === Constants ===

/// Maximum floor(log10(n)) for any non-zero `u256`.
const MAX_LOG10: u8 = 77;
/// 10^2, the base step in the binary-exponentiation log10 ladder.
const TEN_POW_2: u256 = 100;
/// 10^4 = (10^2)^2.
const TEN_POW_4: u256 = TEN_POW_2 * TEN_POW_2;
/// 10^8 = (10^4)^2.
const TEN_POW_8: u256 = TEN_POW_4 * TEN_POW_4;
/// 10^16 = (10^8)^2.
const TEN_POW_16: u256 = TEN_POW_8 * TEN_POW_8;
/// 10^32 = (10^16)^2.
const TEN_POW_32: u256 = TEN_POW_16 * TEN_POW_16;
/// 10^64 = (10^32)^2.
const TEN_POW_64: u256 = TEN_POW_32 * TEN_POW_32;

// === Package Functions ===
Expand Down Expand Up @@ -88,8 +96,8 @@ public(package) macro fun average<$Int>($a: $Int, $b: $Int, $rounding_mode: Roun
/// - `option::none()`.
///
/// #### Aborts
/// - Does not emit custom errors, but will inherit the Move abort that occurs when `$shift` is greater
/// - than or equal to the bit-width of `$Int`.
/// - Does not emit custom errors, but inherits the Move abort that occurs when `$shift` is
/// greater than or equal to the bit-width of `$Int`.
public(package) macro fun checked_shl<$Int>($value: $Int, $shift: u8): Option<$Int> {
let (value, shift) = ($value, $shift);
if (shift == 0) {
Expand Down Expand Up @@ -126,8 +134,8 @@ public(package) macro fun checked_shl<$Int>($value: $Int, $shift: u8): Option<$I
/// - `option::none()`.
///
/// #### Aborts
/// - Does not emit custom errors, but will inherit the Move abort that occurs when `$shift` is greater
/// - than or equal to the bit-width of `$Int`.
/// - Does not emit custom errors, but inherits the Move abort that occurs when `$shift` is
/// greater than or equal to the bit-width of `$Int`.
public(package) macro fun checked_shr<$Int>($value: $Int, $shift: u8): Option<$Int> {
let (value, shift) = ($value, $shift);
let mask = (1_u256 << shift) - 1;
Expand Down Expand Up @@ -155,8 +163,8 @@ public(package) macro fun checked_shr<$Int>($value: $Int, $shift: u8): Option<$I
/// - `$rounding_mode`: Rounding strategy.
///
/// #### Returns
/// - `(overflow, result)` where `overflow` is `true` when the rounded quotient exceeds `u256::MAX` and
/// - `result` carries the rounded value when no overflow occurred.
/// - `(overflow, result)` where `overflow` is `true` when the rounded quotient exceeds `u256::MAX`,
/// and `result` carries the rounded value when no overflow occurred.
///
/// #### Aborts
/// - Propagates the same error codes as the underlying helpers (`EDivideByZero`).
Expand Down Expand Up @@ -191,8 +199,8 @@ public(package) macro fun mul_div<$Int>(
/// - `$rounding_mode`: Rounding strategy drawn from `rounding::RoundingMode`.
///
/// #### Returns
/// - `(overflow, result)` where `overflow` reports that the rounded value cannot fit in 256 bits and
/// - `result` contains the rounded quotient when no overflow occurs.
/// - `(overflow, result)` where `overflow` reports that the rounded value cannot fit in 256 bits,
/// and `result` contains the rounded quotient when no overflow occurs.
///
/// #### Aborts
/// - Does not emit custom errors.
Expand Down Expand Up @@ -238,6 +246,9 @@ public(package) macro fun clz<$Int>($value: $Int, $bit_width: u16): u16 {
/// `$value` and `$modulus` are co-prime. If the inverse does not exist, the function returns
/// `option::none()`.
///
/// #### Generics
/// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`).
///
/// #### Parameters
/// - `$value`: Unsigned integer whose inverse is being computed.
/// - `$modulus`: Modulus for the arithmetic; must be non-zero.
Expand All @@ -247,7 +258,7 @@ public(package) macro fun clz<$Int>($value: $Int, $bit_width: u16): u16 {
/// - `option::none()` when `value` and `modulus` are not co-prime, or when `modulus` is 1.
///
/// #### Aborts
/// - Aborts if `modulus` is zero.
/// - `EZeroModulus` if `$modulus` is zero.
public(package) macro fun inv_mod<$Int>($value: $Int, $modulus: $Int): Option<$Int> {
let value_u256 = ($value as u256);
let modulus_u256 = ($modulus as u256);
Expand All @@ -260,12 +271,18 @@ public(package) macro fun inv_mod<$Int>($value: $Int, $modulus: $Int): Option<$I
/// Uses the shared internal helper that automatically chooses between the fast `u256` path and
/// the wide `u512` implementation. The modulus must be non-zero.
///
/// #### Generics
/// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`).
///
/// #### Parameters
/// - `$a`, `$b`: Unsigned operands.
/// - `$modulus`: Modulus for the arithmetic; must be non-zero.
///
/// #### Returns
/// - The product reduced modulo `$modulus`.
///
/// #### Aborts
/// - `EZeroModulus` if `$modulus` is zero.
public(package) macro fun mul_mod<$Int>($a: $Int, $b: $Int, $modulus: $Int): $Int {
let a_u256 = ($a as u256);
let b_u256 = ($b as u256);
Expand Down Expand Up @@ -481,6 +498,9 @@ public(package) fun log10_floor(value: u256): u8 {
///
/// #### Returns
/// - `(overflow, quotient)` mirroring the macro implementation.
///
/// #### Aborts
/// - `EDivideByZero` if `denominator` is zero.
public(package) fun mul_div_inner(
a: u256,
b: u256,
Expand All @@ -507,8 +527,8 @@ public(package) fun mul_div_inner(
/// - `rounding_mode`: Rounding strategy drawn from `rounding::RoundingMode`.
///
/// #### Returns
/// `(overflow, quotient)` where `overflow` is always `false` for this fast path and `quotient`
/// carries the rounded result.
/// - `(overflow, quotient)` where `overflow` is always `false` for this fast path and `quotient`
/// carries the rounded result.
///
/// #### Aborts
/// - `EDivideByZero` if `denominator` is zero.
Expand Down Expand Up @@ -546,8 +566,8 @@ public(package) fun mul_div_u256_fast(
/// - `rounding_mode`: Rounding strategy drawn from `rounding::RoundingMode`.
///
/// #### Returns
/// - `(overflow, result)` where `overflow` indicates whether the exact (or rounded) quotient exceeds
/// - the `u256` range. `result` is only meaningful when `overflow` is `false`.
/// - `(overflow, result)` where `overflow` indicates whether the exact (or rounded) quotient
/// exceeds the `u256` range. `result` is only meaningful when `overflow` is `false`.
///
/// #### Aborts
/// - `EDivideByZero` if `denominator` is zero.
Expand Down Expand Up @@ -642,8 +662,8 @@ public(package) fun mul_shr_u256_fast(
/// - `rounding_mode`: Rounding strategy drawn from `rounding::RoundingMode`.
///
/// #### Returns
/// - `(overflow, result)` where `overflow` indicates whether the shifted value cannot fit in 256 bits
/// - and `result` contains the shifted and rounded value when no overflow occurred.
/// - `(overflow, result)` where `overflow` indicates whether the shifted value cannot fit in
/// 256 bits and `result` contains the shifted and rounded value when no overflow occurred.
public(package) fun mul_shr_u256_wide(
a: u256,
b: u256,
Expand Down Expand Up @@ -721,8 +741,8 @@ public(package) macro fun sqrt<$Int>($value: $Int, $rounding_mode: RoundingMode)
/// - `rounding_mode`: Rounding strategy.
///
/// #### Returns
/// - `(overflow, result)` where `overflow` is `true` if the rounded value cannot be represented as
/// - `u256`.
/// - `(overflow, result)` where `overflow` is `true` if the rounded value cannot be represented
/// as `u256`.
public(package) fun round_division_result(
result: u256,
denominator: u256,
Expand Down Expand Up @@ -769,8 +789,8 @@ public(package) fun round_division_result(
/// Tie-break: equality goes up (`≥`), i.e., “round half up”.
///
/// #### Returns
/// - The rounded base-2 logarithm: `floor_log + 1` when the midpoint threshold is met, otherwise
/// - `floor_log`.
/// - The rounded base-2 logarithm: `floor_log + 1` when the midpoint threshold is met,
/// otherwise `floor_log`.
public(package) fun round_log2_to_nearest(value: u256, floor_log: u16): u16 {
let threshold_exp = 2 * floor_log + 1;
let max_small = std::u128::max_value!() as u256;
Expand Down Expand Up @@ -813,8 +833,8 @@ public(package) fun round_log2_to_nearest(value: u256, floor_log: u16): u16 {
/// Tie-break: equality goes up (`≥`), i.e., “round half up”.
///
/// #### Returns
/// - The rounded base-256 logarithm: `floor_log + 1` when the midpoint threshold is met, otherwise
/// - `floor_log`.
/// - The rounded base-256 logarithm: `floor_log + 1` when the midpoint threshold is met,
/// otherwise `floor_log`.
public(package) fun round_log256_to_nearest(value: u256, floor_log: u8): u8 {
// For u256 values, floor_log ∈ [0, 31], so `threshold_exp = 8 * floor_log + 4 ≤ 252`
// and the power-of-two threshold fits safely in u256.
Expand Down Expand Up @@ -951,7 +971,11 @@ public(package) fun round_sqrt_result(
/// - `modulus`: Modulus, must be non-zero.
///
/// #### Returns
/// - `option::some(inverse)` when `value` and `modulus` are co-prime, otherwise `option::none()`.
/// - `option::some(inverse)` when `value` and `modulus` are co-prime.
/// - `option::none()` when they are not co-prime, or when `modulus` is 1.
///
/// #### Aborts
/// - `EZeroModulus` if `modulus` is zero.
public(package) fun inv_mod_extended_impl(value: u256, modulus: u256): Option<u256> {
// Guard against invalid modulus values up front.
assert!(modulus != 0, EZeroModulus);
Expand Down
8 changes: 8 additions & 0 deletions math/core/sources/u512.move
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,33 @@ use openzeppelin_math::common;

// === Errors ===

/// Raised when a cross-limb addition carry exceeds the representable range.
#[error(code = 0)]
const ECarryOverflow: vector<u8> = "Cross-limb addition overflowed";
/// Raised when a subtraction borrow underflows the high limb.
#[error(code = 1)]
const EUnderflow: vector<u8> = "Borrow underflowed high limb";
/// Raised when a division or modular operation is given a zero divisor.
#[error(code = 2)]
const EDivideByZero: vector<u8> = "Divisor must be non-zero";
/// Raised when a division remainder leaves non-zero high bits, violating the result invariant.
#[error(code = 3)]
const EInvalidRemainder: vector<u8> = "High remainder bits must be zero";

// === Constants ===

/// Bit-width of each half-limb used when splitting a `u256` into two 128-bit halves.
const HALF_BITS: u8 = 128;
/// Bitmask selecting the low 128 bits of a `u256` value.
const HALF_MASK: u256 = (1u256 << HALF_BITS) - 1;

// === Structs ===

/// Represents a 512-bit unsigned integer as two 256-bit words.
public struct U512 has copy, drop, store {
/// Upper 256 bits.
hi: u256,
/// Lower 256 bits.
lo: u256,
}

Expand Down
42 changes: 42 additions & 0 deletions math/core/sources/vector.move
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,13 @@ public macro fun median<$Int>($vec: &vector<$Int>, $rounding_mode: RoundingMode)
/// The input vector is not mutated. See `median!` for the full contract and the bytecode
/// vs. gas trade-off.
///
/// #### Parameters
/// - `vec`: Reference to the vector whose median is desired.
/// - `rounding_mode`: Rounding strategy, applied only when the length is even.
///
/// #### Returns
/// - The median of `vec`.
///
/// #### Aborts
/// - `EMedianOfEmptyVector` if `vec` is empty.
public fun median_u8(vec: &vector<u8>, rounding_mode: RoundingMode): u8 {
Expand All @@ -433,6 +440,13 @@ public fun median_u8(vec: &vector<u8>, rounding_mode: RoundingMode): u8 {
/// The input vector is not mutated. See `median!` for the full contract and the bytecode
/// vs. gas trade-off.
///
/// #### Parameters
/// - `vec`: Reference to the vector whose median is desired.
/// - `rounding_mode`: Rounding strategy, applied only when the length is even.
///
/// #### Returns
/// - The median of `vec`.
///
/// #### Aborts
/// - `EMedianOfEmptyVector` if `vec` is empty.
public fun median_u16(vec: &vector<u16>, rounding_mode: RoundingMode): u16 {
Expand All @@ -447,6 +461,13 @@ public fun median_u16(vec: &vector<u16>, rounding_mode: RoundingMode): u16 {
/// The input vector is not mutated. See `median!` for the full contract and the bytecode
/// vs. gas trade-off.
///
/// #### Parameters
/// - `vec`: Reference to the vector whose median is desired.
/// - `rounding_mode`: Rounding strategy, applied only when the length is even.
///
/// #### Returns
/// - The median of `vec`.
///
/// #### Aborts
/// - `EMedianOfEmptyVector` if `vec` is empty.
public fun median_u32(vec: &vector<u32>, rounding_mode: RoundingMode): u32 {
Expand All @@ -461,6 +482,13 @@ public fun median_u32(vec: &vector<u32>, rounding_mode: RoundingMode): u32 {
/// The input vector is not mutated. See `median!` for the full contract and the bytecode
/// vs. gas trade-off.
///
/// #### Parameters
/// - `vec`: Reference to the vector whose median is desired.
/// - `rounding_mode`: Rounding strategy, applied only when the length is even.
///
/// #### Returns
/// - The median of `vec`.
///
/// #### Aborts
/// - `EMedianOfEmptyVector` if `vec` is empty.
public fun median_u64(vec: &vector<u64>, rounding_mode: RoundingMode): u64 {
Expand All @@ -475,6 +503,13 @@ public fun median_u64(vec: &vector<u64>, rounding_mode: RoundingMode): u64 {
/// The input vector is not mutated. See `median!` for the full contract and the bytecode
/// vs. gas trade-off.
///
/// #### Parameters
/// - `vec`: Reference to the vector whose median is desired.
/// - `rounding_mode`: Rounding strategy, applied only when the length is even.
///
/// #### Returns
/// - The median of `vec`.
///
/// #### Aborts
/// - `EMedianOfEmptyVector` if `vec` is empty.
public fun median_u128(vec: &vector<u128>, rounding_mode: RoundingMode): u128 {
Expand All @@ -489,6 +524,13 @@ public fun median_u128(vec: &vector<u128>, rounding_mode: RoundingMode): u128 {
/// The input vector is not mutated. See `median!` for the full contract and the bytecode
/// vs. gas trade-off.
///
/// #### Parameters
/// - `vec`: Reference to the vector whose median is desired.
/// - `rounding_mode`: Rounding strategy, applied only when the length is even.
///
/// #### Returns
/// - The median of `vec`.
///
/// #### Aborts
/// - `EMedianOfEmptyVector` if `vec` is empty.
public fun median_u256(vec: &vector<u256>, rounding_mode: RoundingMode): u256 {
Expand Down
2 changes: 1 addition & 1 deletion math/core/tests/decimal_scaling_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module openzeppelin_math::decimal_scaling_tests;
use openzeppelin_math::decimal_scaling;
use std::unit_test::assert_eq;

// === Test Helpers ===
// === Test-Only Helpers ===

/// Build a test value with specified base and decimal places.
///
Expand Down
Loading
Loading