Skip to content
Open
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
3 changes: 3 additions & 0 deletions math/fixed_point/Move.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
name = "openzeppelin_fp_math"
edition = "2024"

[dependencies]
openzeppelin_math = { local = "../core" }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be imported with 3 sections?
1.mainnet
2. testnet
3. dev-dependencies?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach should copy/paste the function at deployment so there's no environment linking. What I had in mind was linking through the mvr not locally. But if we confirm with Sui (and we must before merging) that this indeed copies only the required function source code and not the whole math package we can continue with this approach.


[addresses]
openzeppelin_fp_math = "0x0"
8 changes: 7 additions & 1 deletion math/fixed_point/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Fixed-point decimal types with 9 decimals (10^9), matching Sui coin precision.

## Operations

- Arithmetic: `add`, `sub`, `mul`, `mul_trunc`, `mul_away`, `div`, `div_trunc`, `div_away`, `pow`, `unchecked_add`, `unchecked_sub`, `mod`
- Arithmetic: `add`, `sub`, `mul`, `mul_trunc`, `mul_away`, `div`, `div_trunc`, `div_away`, `pow`, `unchecked_add`, `unchecked_sub`, `mod`, `sqrt`
- Comparison: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `is_zero`
- `UD30x9` also exposes bitwise helpers: `and`, `and2`, `or`, `xor`, `not`, `lshift`, `rshift`, `unchecked_lshift`, `unchecked_rshift`

Expand All @@ -34,6 +34,12 @@ Rule of thumb:
The core `wrap` / `unwrap` APIs are **raw casts**. They preserve the
underlying fixed-point representation and do not multiply or divide by `10^9`.

- `u128 -> UD30x9`: `into_UD30x9`
- `UD30x9 -> SD29x9`: `into_SD29x9`, `try_into_SD29x9`
- `SD29x9 -> UD30x9`: `into_UD30x9`, `try_into_UD30x9`
- Constructors: `zero`, `one`, `max`, `wrap`
- `SD29x9` only: `min`, `from_bits`

```rust
use openzeppelin_fp_math::{sd29x9, ud30x9};

Expand Down
1 change: 1 addition & 0 deletions math/fixed_point/sources/sd29x9/sd29x9.move
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public use fun openzeppelin_fp_math::sd29x9_base::mul_away as SD29x9.mul_away;
public use fun openzeppelin_fp_math::sd29x9_base::mul_trunc as SD29x9.mul_trunc;
public use fun openzeppelin_fp_math::sd29x9_base::negate as SD29x9.negate;
public use fun openzeppelin_fp_math::sd29x9_base::pow as SD29x9.pow;
public use fun openzeppelin_fp_math::sd29x9_base::sqrt as SD29x9.sqrt;
public use fun openzeppelin_fp_math::sd29x9_base::rem as SD29x9.rem;
public use fun openzeppelin_fp_math::sd29x9_base::sub as SD29x9.sub;
public use fun openzeppelin_fp_math::sd29x9_base::unchecked_add as SD29x9.unchecked_add;
Expand Down
33 changes: 31 additions & 2 deletions math/fixed_point/sources/sd29x9/sd29x9_base.move
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ module openzeppelin_fp_math::sd29x9_base;
use openzeppelin_fp_math::common;
use openzeppelin_fp_math::sd29x9::{SD29x9, from_bits, zero, min, one, two_complement, wrap};
use openzeppelin_fp_math::ud30x9::{Self, UD30x9};
use openzeppelin_math::rounding;
use openzeppelin_math::u256;

// === Errors ===

Expand All @@ -21,15 +23,17 @@ const ECannotBeConvertedToUD30x9: vector<u8> = "Value cannot be converted to UD3
#[error(code = 2)]
const EDivideByZero: vector<u8> = "Divisor must be non-zero";

/// Cannot compute square root of a negative value
#[error(code = 3)]
const ENegativeSqrt: vector<u8> = "Cannot compute square root of a negative value";

// === Structs ===

public struct Components has copy, drop {
neg: bool,
mag: u256,
}

// === Public Functions ===

// === Conversion ===

/// Converts a `SD29x9` value to a `UD30x9` value.
Expand Down Expand Up @@ -64,6 +68,8 @@ public fun try_into_UD30x9(x: SD29x9): Option<UD30x9> {
}
}

// === Public Functions ===

/// Returns the absolute value of a `SD29x9`.
///
/// #### Parameters
Expand Down Expand Up @@ -469,6 +475,29 @@ public fun pow(x: SD29x9, exp: u8): SD29x9 {
result.wrap_components()
}

/// Computes the square root of a `SD29x9` value.
///
/// The result is the largest `SD29x9` value `r` such that `r * r <= x`. In other words, the
/// result is truncated (rounded down) to the nearest representable `SD29x9` value.
///
/// #### Parameters
/// - `x`: Input value.
///
/// #### Returns
/// - The non-negative square root of `x`, rounded down to the nearest representable `SD29x9`
/// value.
///
/// #### Aborts
/// - Aborts if `x` is negative.
public fun sqrt(x: SD29x9): SD29x9 {
let Components { neg, mag } = decompose(x.unwrap());
assert!(!neg, ENegativeSqrt);
// Multiply by SCALE to preserve 9 decimal places of precision through the square root:
// sqrt(mag / SCALE) = sqrt(mag * SCALE) / SCALE
let result = u256::sqrt(mag * common::scale_u256!(), rounding::down());
wrap_components(Components { neg: false, mag: result })
}

/// Returns the arithmetic negation of `x`.
///
/// #### Parameters
Expand Down
1 change: 1 addition & 0 deletions math/fixed_point/sources/ud30x9/ud30x9.move
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public use fun openzeppelin_fp_math::ud30x9_base::mul as UD30x9.mul;
public use fun openzeppelin_fp_math::ud30x9_base::mul_away as UD30x9.mul_away;
public use fun openzeppelin_fp_math::ud30x9_base::mul_trunc as UD30x9.mul_trunc;
public use fun openzeppelin_fp_math::ud30x9_base::pow as UD30x9.pow;
public use fun openzeppelin_fp_math::ud30x9_base::sqrt as UD30x9.sqrt;
public use fun openzeppelin_fp_math::ud30x9_base::sub as UD30x9.sub;
public use fun openzeppelin_fp_math::ud30x9_base::unchecked_add as UD30x9.unchecked_add;
public use fun openzeppelin_fp_math::ud30x9_base::unchecked_sub as UD30x9.unchecked_sub;
Expand Down
26 changes: 23 additions & 3 deletions math/fixed_point/sources/ud30x9/ud30x9_base.move
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ module openzeppelin_fp_math::ud30x9_base;

use openzeppelin_fp_math::common;
use openzeppelin_fp_math::sd29x9::{Self, SD29x9};
use openzeppelin_fp_math::ud30x9::{Self, UD30x9, wrap, one};
use openzeppelin_fp_math::ud30x9::{UD30x9, wrap, zero, one};
use openzeppelin_math::rounding;
use openzeppelin_math::u256;

// === Errors ===

Expand Down Expand Up @@ -236,7 +238,7 @@ public fun lshift(x: UD30x9, bits: u8): UD30x9 {
/// - Otherwise, the result of shifting the `x`'s raw bits left by `bits`.
public fun unchecked_lshift(x: UD30x9, bits: u8): UD30x9 {
if (bits >= 128) {
return ud30x9::zero()
return zero()
};
wrap(x.unwrap() << bits)
}
Expand Down Expand Up @@ -454,6 +456,24 @@ public fun pow(x: UD30x9, exp: u8): UD30x9 {
wrap_u256(result)
}

/// Computes the square root of a `UD30x9` value.
///
/// The result is the largest `UD30x9` value `r` such that `r * r <= x`. In other words, the
/// result is truncated (rounded down) to the nearest representable `UD30x9` value.
///
/// #### Parameters
/// - `x`: Input value.
///
/// #### Returns
/// - The square root of `x`, rounded down to the nearest representable `UD30x9` value.
public fun sqrt(x: UD30x9): UD30x9 {
let raw = x.unwrap() as u256;
// Multiply by SCALE to preserve 9 decimal places of precision through the square root:
// sqrt(raw / SCALE) = sqrt(raw * SCALE) / SCALE
let result = u256::sqrt(raw * common::scale_u256!(), rounding::down());
wrap(result as u128)
}

/// Checks whether two `UD30x9` values are not equal.
///
/// #### Parameters
Expand Down Expand Up @@ -520,7 +540,7 @@ public fun rshift(x: UD30x9, bits: u8): UD30x9 {
/// - Otherwise, the result of shifting the `x`'s raw bits right by `bits`.
public fun unchecked_rshift(x: UD30x9, bits: u8): UD30x9 {
if (bits >= 128) {
return ud30x9::zero()
return zero()
};
wrap(x.unwrap() >> bits)
}
Expand Down
114 changes: 114 additions & 0 deletions math/fixed_point/tests/sd29x9_tests/sqrt_tests.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#[test_only]
module openzeppelin_fp_math::sd29x9_sqrt_tests;

use openzeppelin_fp_math::sd29x9;
use openzeppelin_fp_math::sd29x9_base;
use openzeppelin_fp_math::sd29x9_test_helpers::{pos, neg};
use std::unit_test::assert_eq;

const SCALE: u128 = 1_000_000_000;
const MAX_POSITIVE_VALUE: u128 = 0x7FFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF;

// ==== Tests ====

#[test]
fun sqrt_of_zero_is_zero() {
assert_eq!(sd29x9::zero().sqrt(), sd29x9::zero());
}

#[test]
fun sqrt_of_positive_one() {
assert_eq!(sd29x9::one().sqrt(), sd29x9::one());
}

#[test]
fun sqrt_of_positive_perfect_squares() {
// sqrt(+4.0) = +2.0
assert_eq!(pos(4 * SCALE).sqrt(), pos(2 * SCALE));
// sqrt(+9.0) = +3.0
assert_eq!(pos(9 * SCALE).sqrt(), pos(3 * SCALE));
// sqrt(+25.0) = +5.0
assert_eq!(pos(25 * SCALE).sqrt(), pos(5 * SCALE));
// sqrt(+100.0) = +10.0
assert_eq!(pos(100 * SCALE).sqrt(), pos(10 * SCALE));
// sqrt(+10000.0) = +100.0
assert_eq!(pos(10_000 * SCALE).sqrt(), pos(100 * SCALE));
}

#[test]
fun sqrt_of_positive_fractional_squares() {
// sqrt(+0.25) = +0.5
assert_eq!(pos(250_000_000).sqrt(), pos(500_000_000));
// sqrt(+0.01) = +0.1
assert_eq!(pos(10_000_000).sqrt(), pos(100_000_000));
// sqrt(+2.25) = +1.5
assert_eq!(pos(2_250_000_000).sqrt(), pos(1_500_000_000));
}

#[test]
fun sqrt_truncates_irrational_results() {
// sqrt(+2.0) = +1.414213562 (truncated)
assert_eq!(pos(2 * SCALE).sqrt(), pos(1_414_213_562));
// sqrt(+3.0) = +1.732050807 (truncated)
assert_eq!(pos(3 * SCALE).sqrt(), pos(1_732_050_807));
// sqrt(+5.0) = +2.236067977 (truncated)
assert_eq!(pos(5 * SCALE).sqrt(), pos(2_236_067_977));
}

#[test]
fun sqrt_of_max_positive() {
// sqrt(sd29x9::max()) should not abort and satisfy the floor property
let result = sd29x9::max().sqrt();
let r = result.unwrap() as u256;
let max_scaled = (sd29x9::max().unwrap() as u256) * (SCALE as u256);
assert!(r * r <= max_scaled);
assert!((r + 1) * (r + 1) > max_scaled);
}

#[random_test]
fun sqrt_result_is_always_non_negative(raw: u128) {
let raw = raw % (MAX_POSITIVE_VALUE + 1);
let result = sd29x9::wrap(raw, false).sqrt();
// Result is non-negative: raw bits should not have sign bit set
assert!(result.unwrap() <= MAX_POSITIVE_VALUE);
}

#[random_test]
fun sqrt_floor_invariant(raw: u128) {
let raw = raw % (MAX_POSITIVE_VALUE + 1);
let result = sd29x9::wrap(raw, false).sqrt();
// Floor property: r^2 <= x * SCALE < (r + 1)^2
let r = result.unwrap() as u256;
let scaled = (raw as u256) * (SCALE as u256);
assert!(r * r <= scaled);
assert!((r + 1) * (r + 1) > scaled);
Comment thread
immrsd marked this conversation as resolved.
}

#[test]
fun sqrt_squared_roundtrip_for_perfect_squares() {
let values = vector[
pos(4 * SCALE),
pos(9 * SCALE),
pos(25 * SCALE),
pos(250_000_000), // 0.25
];
values.destroy!(|x| {
let root = x.sqrt();
assert_eq!(root.mul(root), x);
});
}

#[test, expected_failure(abort_code = sd29x9_base::ENegativeSqrt)]
fun sqrt_of_negative_aborts() {
neg(SCALE).sqrt();
}

#[test, expected_failure(abort_code = sd29x9_base::ENegativeSqrt)]
fun sqrt_of_small_negative_aborts() {
neg(1).sqrt();
}

#[test, expected_failure(abort_code = sd29x9_base::ENegativeSqrt)]
fun sqrt_of_min_value_aborts() {
sd29x9::min().sqrt();
}
Loading
Loading