From e0db19ff4a0845a0d786ab960ce31575510f690a Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 19 May 2026 17:58:55 +0200 Subject: [PATCH 01/12] refactor: replace is_power_of_ten lookup tables with log10_floor + pow macro Closes #295. Public API unchanged. The new public(package) macro `is_power_of_ten!` in internal/macros.move computes the predicate via the existing log10_floor and std::u256::pow primitives, eliminating the duplicated power-of-ten constants that previously caused the missing 10^77 entry (fixed in #291). Only u128 and u256 wrappers are retargeted per the team's gas-report decision; u8 through u64 keep their inline disjunctions. --- CHANGELOG.md | 6 + math/core/sources/internal/macros.move | 24 ++++ math/core/sources/u128.move | 46 +----- math/core/sources/u256.move | 85 +---------- math/core/tests/macros/is_power_of_ten.move | 149 ++++++++++++++++++++ math/core/tests/u128_tests.move | 7 +- math/core/tests/u256_tests.move | 7 +- 7 files changed, 187 insertions(+), 137 deletions(-) create mode 100644 math/core/tests/macros/is_power_of_ten.move diff --git a/CHANGELOG.md b/CHANGELOG.md index 264f81c7..a74eede1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - `sqrt` for `UD30x9` and `SD29x9` with round-down semantics. (#286) - `log2`, `ln`, `log10` for `UD30x9` (rounds down) and `SD29x9` (rounds toward zero). (#320) +### `openzeppelin_math` + +#### Changed + +- `u128::is_power_of_ten` and `u256::is_power_of_ten` now compute the result via `log10_floor` and `pow` instead of a hardcoded lookup table, eliminating the duplicated power-of-ten constants that previously caused the missing `10^77` entry (see #291). Public API unchanged. + ## 1.1.0 (21-04-2026) ### `openzeppelin_fp_math` diff --git a/math/core/sources/internal/macros.move b/math/core/sources/internal/macros.move index c7e9b37b..33dec851 100644 --- a/math/core/sources/internal/macros.move +++ b/math/core/sources/internal/macros.move @@ -404,6 +404,30 @@ public(package) macro fun log10<$Int>($value: $Int, $rounding_mode: RoundingMode } } +/// Test whether `$n` is an exact non-negative integer power of ten (`10^k` for some +/// `k >= 0`). +/// +/// The macro widens the input to `u256`, short-circuits to `false` on zero, then verifies +/// `n == 10^log10_floor(n)`. Mirrors the structure of `log10` above: widen → early-return +/// on zero → let-bound `log10_floor` result → equality check via `std::u256::pow`. +/// +/// #### Generics +/// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`). +/// +/// #### Parameters +/// - `$n`: The unsigned integer to test. +/// +/// #### Returns +/// - `true` if `$n` equals `10^k` for some `k >= 0`, otherwise `false`. +public(package) macro fun is_power_of_ten<$Int>($n: $Int): bool { + let n = $n as u256; + if (n == 0) { + return false + }; + let exp = log10_floor(n); + n == std::u256::pow(10, exp) +} + /// Compute floor(log10(value)) using binary search over powers of 10. /// /// This helper uses precomputed constants (`TEN_POW_2`, `TEN_POW_4`, etc.) to efficiently diff --git a/math/core/sources/u128.move b/math/core/sources/u128.move index f5519a50..45ac06e0 100644 --- a/math/core/sources/u128.move +++ b/math/core/sources/u128.move @@ -217,7 +217,6 @@ public fun mul_mod(a: u128, b: u128, modulus: u128): u128 { /// Returns `true` if `n` is a power of ten. /// -/// Uses a lookup table with binary search for efficiency. /// For `u128`, valid powers of ten range from 10^0 to 10^38. /// /// #### Parameters @@ -226,48 +225,5 @@ public fun mul_mod(a: u128, b: u128, modulus: u128): u128 { /// #### Returns /// - `true` if `n` is a power of ten within the `u128` range, otherwise `false`. public fun is_power_of_ten(n: u128): bool { - // Powers of 10 from 10^0 to 10^38 for u128 - let powers = vector[ - 1u128, - 10u128, - 100u128, - 1000u128, - 10000u128, - 100000u128, - 1000000u128, - 10000000u128, - 100000000u128, - 1000000000u128, - 10000000000u128, - 100000000000u128, - 1000000000000u128, - 10000000000000u128, - 100000000000000u128, - 1000000000000000u128, - 10000000000000000u128, - 100000000000000000u128, - 1000000000000000000u128, - 10000000000000000000u128, - 100000000000000000000u128, - 1000000000000000000000u128, - 10000000000000000000000u128, - 100000000000000000000000u128, - 1000000000000000000000000u128, - 10000000000000000000000000u128, - 100000000000000000000000000u128, - 1000000000000000000000000000u128, - 10000000000000000000000000000u128, - 100000000000000000000000000000u128, - 1000000000000000000000000000000u128, - 10000000000000000000000000000000u128, - 100000000000000000000000000000000u128, - 1000000000000000000000000000000000u128, - 10000000000000000000000000000000000u128, - 100000000000000000000000000000000000u128, - 1000000000000000000000000000000000000u128, - 10000000000000000000000000000000000000u128, - 100000000000000000000000000000000000000u128, - ]; - - macros::binary_search!(powers, n) + macros::is_power_of_ten!(n) } diff --git a/math/core/sources/u256.move b/math/core/sources/u256.move index 8fba9414..321b515d 100644 --- a/math/core/sources/u256.move +++ b/math/core/sources/u256.move @@ -226,7 +226,6 @@ public fun mul_mod(a: u256, b: u256, modulus: u256): u256 { /// Returns `true` if `n` is a power of ten. /// -/// Uses a lookup table with binary search for efficiency. /// For `u256`, valid powers of ten range from 10^0 to 10^77. /// /// #### Parameters @@ -235,87 +234,5 @@ public fun mul_mod(a: u256, b: u256, modulus: u256): u256 { /// #### Returns /// - `true` if `n` is a power of ten within the `u256` range, otherwise `false`. public fun is_power_of_ten(n: u256): bool { - // Powers of 10 from 10^0 to 10^77 for u256 - let powers = vector[ - 1u256, - 10u256, - 100u256, - 1000u256, - 10000u256, - 100000u256, - 1000000u256, - 10000000u256, - 100000000u256, - 1000000000u256, - 10000000000u256, - 100000000000u256, - 1000000000000u256, - 10000000000000u256, - 100000000000000u256, - 1000000000000000u256, - 10000000000000000u256, - 100000000000000000u256, - 1000000000000000000u256, - 10000000000000000000u256, - 100000000000000000000u256, - 1000000000000000000000u256, - 10000000000000000000000u256, - 100000000000000000000000u256, - 1000000000000000000000000u256, - 10000000000000000000000000u256, - 100000000000000000000000000u256, - 1000000000000000000000000000u256, - 10000000000000000000000000000u256, - 100000000000000000000000000000u256, - 1000000000000000000000000000000u256, - 10000000000000000000000000000000u256, - 100000000000000000000000000000000u256, - 1000000000000000000000000000000000u256, - 10000000000000000000000000000000000u256, - 100000000000000000000000000000000000u256, - 1000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000000000000000000000000u256, - 1000000000000000000000000000000000000000000000000000000000000000000000000000u256, - 10000000000000000000000000000000000000000000000000000000000000000000000000000u256, - 100000000000000000000000000000000000000000000000000000000000000000000000000000u256, - ]; - - macros::binary_search!(powers, n) + macros::is_power_of_ten!(n) } diff --git a/math/core/tests/macros/is_power_of_ten.move b/math/core/tests/macros/is_power_of_ten.move new file mode 100644 index 00000000..17c8b011 --- /dev/null +++ b/math/core/tests/macros/is_power_of_ten.move @@ -0,0 +1,149 @@ +#[test_only] +module openzeppelin_math::is_power_of_ten; + +use openzeppelin_math::macros; +use std::unit_test::assert_eq; + +// INV-R1: explicit zero guard returns false on u128 and u256 instantiations. +#[test] +fun is_power_of_ten_returns_false_for_zero_u128() { + assert_eq!(macros::is_power_of_ten!(0u128), false); +} + +#[test] +fun is_power_of_ten_returns_false_for_zero_u256() { + assert_eq!(macros::is_power_of_ten!(0u256), false); +} + +// INV-R2: 10^0 = 1 is a power of ten. +#[test] +fun is_power_of_ten_returns_true_for_one_u128() { + assert_eq!(macros::is_power_of_ten!(1u128), true); +} + +#[test] +fun is_power_of_ten_returns_true_for_one_u256() { + assert_eq!(macros::is_power_of_ten!(1u256), true); +} + +// INV-R3: positive cases — sampled exponents covering low, mid, and u128 boundary. +#[test] +fun is_power_of_ten_returns_true_for_sampled_powers_u128() { + assert_eq!(macros::is_power_of_ten!(10u128), true); // 10^1 + assert_eq!(macros::is_power_of_ten!(100000u128), true); // 10^5 + assert_eq!(macros::is_power_of_ten!(10000000000000000000u128), true); // 10^19 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u128), true); // 10^38, u128 boundary +} + +// INV-R3: positive cases — sampled exponents covering low, mid, and u256 boundary +// (including values beyond the u128 range to exercise generic widening at u256 width). +#[test] +fun is_power_of_ten_returns_true_for_sampled_powers_u256() { + assert_eq!(macros::is_power_of_ten!(10u256), true); // 10^1 + assert_eq!(macros::is_power_of_ten!(100000u256), true); // 10^5 + assert_eq!(macros::is_power_of_ten!(10000000000000000000u256), true); // 10^19 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u256), true); // 10^38 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000000000000000u256), true); // 10^50 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000000000000000000000000000000000000000000u256), true); // 10^77, u256 boundary +} + +// INV-R4: boundary negatives — values one below and one above sampled powers. +#[test] +fun is_power_of_ten_returns_false_at_boundaries_u128() { + assert_eq!(macros::is_power_of_ten!(9u128), false); // 10^1 - 1 + assert_eq!(macros::is_power_of_ten!(11u128), false); // 10^1 + 1 + assert_eq!(macros::is_power_of_ten!(99u128), false); // 10^2 - 1 + assert_eq!(macros::is_power_of_ten!(101u128), false); // 10^2 + 1 + assert_eq!(macros::is_power_of_ten!(99999999999999999999999999999999999999u128), false); // 10^38 - 1 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000001u128), false); // 10^38 + 1 +} + +#[test] +fun is_power_of_ten_returns_false_at_boundaries_u256() { + assert_eq!(macros::is_power_of_ten!(9u256), false); + assert_eq!(macros::is_power_of_ten!(11u256), false); + assert_eq!(macros::is_power_of_ten!(99u256), false); + assert_eq!(macros::is_power_of_ten!(101u256), false); + assert_eq!(macros::is_power_of_ten!(99999999999999999999999999999999999999999999999999999999999999999999999999999u256), false); // 10^77 - 1 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000000000000000000000000000000000000000001u256), false); // 10^77 + 1 +} + +// INV-R4: arbitrary non-powers. +#[test] +fun is_power_of_ten_returns_false_for_non_powers_u128() { + assert_eq!(macros::is_power_of_ten!(2u128), false); + assert_eq!(macros::is_power_of_ten!(3u128), false); + assert_eq!(macros::is_power_of_ten!(999u128), false); +} + +#[test] +fun is_power_of_ten_returns_false_for_non_powers_u256() { + assert_eq!(macros::is_power_of_ten!(2u256), false); + assert_eq!(macros::is_power_of_ten!(3u256), false); + assert_eq!(macros::is_power_of_ten!(999u256), false); +} + +// INV-T1: macro is generic over `$Int` for any unsigned integer type. Production code +// only instantiates the macro at u128 and u256, but the macro body is intrinsically +// type-generic — these tests pin down the contract at the smaller widths. +#[test] +fun is_power_of_ten_macro_works_on_u8() { + assert_eq!(macros::is_power_of_ten!(0u8), false); + assert_eq!(macros::is_power_of_ten!(1u8), true); + assert_eq!(macros::is_power_of_ten!(10u8), true); + assert_eq!(macros::is_power_of_ten!(100u8), true); + assert_eq!(macros::is_power_of_ten!(11u8), false); + assert_eq!(macros::is_power_of_ten!(std::u8::max_value!()), false); +} + +#[test] +fun is_power_of_ten_macro_works_on_u16() { + assert_eq!(macros::is_power_of_ten!(0u16), false); + assert_eq!(macros::is_power_of_ten!(1u16), true); + assert_eq!(macros::is_power_of_ten!(10000u16), true); + assert_eq!(macros::is_power_of_ten!(9999u16), false); + assert_eq!(macros::is_power_of_ten!(std::u16::max_value!()), false); +} + +#[test] +fun is_power_of_ten_macro_works_on_u32() { + assert_eq!(macros::is_power_of_ten!(0u32), false); + assert_eq!(macros::is_power_of_ten!(1u32), true); + assert_eq!(macros::is_power_of_ten!(1000000000u32), true); + assert_eq!(macros::is_power_of_ten!(999999999u32), false); + assert_eq!(macros::is_power_of_ten!(std::u32::max_value!()), false); +} + +#[test] +fun is_power_of_ten_macro_works_on_u64() { + assert_eq!(macros::is_power_of_ten!(0u64), false); + assert_eq!(macros::is_power_of_ten!(1u64), true); + assert_eq!(macros::is_power_of_ten!(10000000000000000000u64), true); // 10^19 + assert_eq!(macros::is_power_of_ten!(9999999999999999999u64), false); + assert_eq!(macros::is_power_of_ten!(std::u64::max_value!()), false); +} + +// INV-R3 + INV-R4 at the log10_floor cascade boundary k=64. +// log10_floor's outermost dispatch branch is `if (value >= TEN_POW_64)`. No other test in +// the package (neither is_power_of_ten nor log10's own test suite) exercises this exact +// threshold. A regression in log10_floor's k=64 branch would silently skew the predicate +// for every exponent at or above 64 without any existing test catching it. +#[test] +fun is_power_of_ten_at_log10_floor_cascade_boundary_u256() { + assert_eq!(macros::is_power_of_ten!(10000000000000000000000000000000000000000000000000000000000000000u256), true); // 10^64 + assert_eq!(macros::is_power_of_ten!(9999999999999999999999999999999999999999999999999999999999999999u256), false); // 10^64 - 1 + assert_eq!(macros::is_power_of_ten!(10000000000000000000000000000000000000000000000000000000000000001u256), false); // 10^64 + 1 +} + +// INV-R5 + INV-R6: TYPE_MAX is not a power of ten, and the macro must not abort. +// For u256_max, log10_floor returns 77 — this exercises the largest possible exponent +// and confirms std::u256::pow(10, 77) does not overflow. +#[test] +fun is_power_of_ten_returns_false_for_u128_max() { + assert_eq!(macros::is_power_of_ten!(std::u128::max_value!()), false); +} + +#[test] +fun is_power_of_ten_returns_false_for_u256_max() { + assert_eq!(macros::is_power_of_ten!(std::u256::max_value!()), false); +} diff --git a/math/core/tests/u128_tests.move b/math/core/tests/u128_tests.move index 0c690094..0fc26c71 100644 --- a/math/core/tests/u128_tests.move +++ b/math/core/tests/u128_tests.move @@ -827,15 +827,14 @@ fun is_power_of_ten_edge_cases() { } #[test] -fun is_power_of_ten_binary_search_paths() { - // Test values to exercise different binary search paths - // These test values at different positions in the lookup table +fun is_power_of_ten_diverse_inputs() { + // Sampled powers of ten across the u128 range. assert_eq!(u128::is_power_of_ten(100000), true); // 10^5 - lower middle assert_eq!(u128::is_power_of_ten(10000000000000), true); // 10^13 - middle assert_eq!(u128::is_power_of_ten(1000000000000000000000000), true); // 10^24 - upper middle assert_eq!(u128::is_power_of_ten(10000000000000000000000000000000), true); // 10^31 - upper range - // Test non-powers at various positions to exercise binary search failure paths + // Non-powers sampled at positions between consecutive powers of ten. assert_eq!(u128::is_power_of_ten(3), false); // Less than first non-1 power assert_eq!(u128::is_power_of_ten(15), false); // Between 10 and 100 assert_eq!(u128::is_power_of_ten(150), false); // Between 100 and 1000 diff --git a/math/core/tests/u256_tests.move b/math/core/tests/u256_tests.move index d1be8874..1f2653ef 100644 --- a/math/core/tests/u256_tests.move +++ b/math/core/tests/u256_tests.move @@ -918,16 +918,15 @@ fun is_power_of_ten_edge_cases() { } #[test] -fun is_power_of_ten_binary_search_paths() { - // Test values to exercise different binary search paths - // These test values at different positions in the lookup table +fun is_power_of_ten_diverse_inputs() { + // Sampled powers of ten across the u256 range. assert_eq!(u256::is_power_of_ten(100000), true); // 10^5 - middle range assert_eq!(u256::is_power_of_ten(10000000000000), true); // 10^13 - middle range assert_eq!(u256::is_power_of_ten(100000000000000000000000000000000), true); // 10^32 - upper middle assert_eq!(u256::is_power_of_ten(1000000000000000000000000000000000000000), true); // 10^39 - upper range assert_eq!(u256::is_power_of_ten(10000000000000000000000000000000000000000000000000000), true); // 10^52 - high range - // Test non-powers at various positions to exercise binary search failure paths + // Non-powers sampled at positions between consecutive powers of ten. assert_eq!(u256::is_power_of_ten(3), false); // Less than first non-1 power assert_eq!(u256::is_power_of_ten(15), false); // Between 10 and 100 assert_eq!(u256::is_power_of_ten(150), false); // Between 100 and 1000 From 826e5f1abe5fd206f9af30202f4666e2009a48a8 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 19 May 2026 18:16:16 +0200 Subject: [PATCH 02/12] style: apply prettier-move and drop INV-N comment markers --- math/core/tests/macros/is_power_of_ten.move | 105 +++++++++++++------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/math/core/tests/macros/is_power_of_ten.move b/math/core/tests/macros/is_power_of_ten.move index 17c8b011..f10554c0 100644 --- a/math/core/tests/macros/is_power_of_ten.move +++ b/math/core/tests/macros/is_power_of_ten.move @@ -4,7 +4,7 @@ module openzeppelin_math::is_power_of_ten; use openzeppelin_math::macros; use std::unit_test::assert_eq; -// INV-R1: explicit zero guard returns false on u128 and u256 instantiations. +// Explicit zero guard returns false on u128 and u256 instantiations. #[test] fun is_power_of_ten_returns_false_for_zero_u128() { assert_eq!(macros::is_power_of_ten!(0u128), false); @@ -15,7 +15,7 @@ fun is_power_of_ten_returns_false_for_zero_u256() { assert_eq!(macros::is_power_of_ten!(0u256), false); } -// INV-R2: 10^0 = 1 is a power of ten. +// 10^0 = 1 is a power of ten. #[test] fun is_power_of_ten_returns_true_for_one_u128() { assert_eq!(macros::is_power_of_ten!(1u128), true); @@ -26,36 +26,44 @@ fun is_power_of_ten_returns_true_for_one_u256() { assert_eq!(macros::is_power_of_ten!(1u256), true); } -// INV-R3: positive cases — sampled exponents covering low, mid, and u128 boundary. +// Positive cases — sampled exponents covering low, mid, and u128 boundary. #[test] fun is_power_of_ten_returns_true_for_sampled_powers_u128() { - assert_eq!(macros::is_power_of_ten!(10u128), true); // 10^1 - assert_eq!(macros::is_power_of_ten!(100000u128), true); // 10^5 - assert_eq!(macros::is_power_of_ten!(10000000000000000000u128), true); // 10^19 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u128), true); // 10^38, u128 boundary + assert_eq!(macros::is_power_of_ten!(10u128), true); // 10^1 + assert_eq!(macros::is_power_of_ten!(100000u128), true); // 10^5 + assert_eq!(macros::is_power_of_ten!(10000000000000000000u128), true); // 10^19 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u128), true); // 10^38, u128 boundary } -// INV-R3: positive cases — sampled exponents covering low, mid, and u256 boundary +// Positive cases — sampled exponents covering low, mid, and u256 boundary // (including values beyond the u128 range to exercise generic widening at u256 width). #[test] fun is_power_of_ten_returns_true_for_sampled_powers_u256() { - assert_eq!(macros::is_power_of_ten!(10u256), true); // 10^1 - assert_eq!(macros::is_power_of_ten!(100000u256), true); // 10^5 - assert_eq!(macros::is_power_of_ten!(10000000000000000000u256), true); // 10^19 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u256), true); // 10^38 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000000000000000u256), true); // 10^50 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000000000000000000000000000000000000000000u256), true); // 10^77, u256 boundary -} - -// INV-R4: boundary negatives — values one below and one above sampled powers. + assert_eq!(macros::is_power_of_ten!(10u256), true); // 10^1 + assert_eq!(macros::is_power_of_ten!(100000u256), true); // 10^5 + assert_eq!(macros::is_power_of_ten!(10000000000000000000u256), true); // 10^19 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u256), true); // 10^38 + assert_eq!( + macros::is_power_of_ten!(100000000000000000000000000000000000000000000000000u256), + true, + ); // 10^50 + assert_eq!( + macros::is_power_of_ten!( + 100000000000000000000000000000000000000000000000000000000000000000000000000000u256, + ), + true, + ); // 10^77, u256 boundary +} + +// Boundary negatives — values one below and one above sampled powers. #[test] fun is_power_of_ten_returns_false_at_boundaries_u128() { - assert_eq!(macros::is_power_of_ten!(9u128), false); // 10^1 - 1 - assert_eq!(macros::is_power_of_ten!(11u128), false); // 10^1 + 1 - assert_eq!(macros::is_power_of_ten!(99u128), false); // 10^2 - 1 - assert_eq!(macros::is_power_of_ten!(101u128), false); // 10^2 + 1 - assert_eq!(macros::is_power_of_ten!(99999999999999999999999999999999999999u128), false); // 10^38 - 1 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000001u128), false); // 10^38 + 1 + assert_eq!(macros::is_power_of_ten!(9u128), false); // 10^1 - 1 + assert_eq!(macros::is_power_of_ten!(11u128), false); // 10^1 + 1 + assert_eq!(macros::is_power_of_ten!(99u128), false); // 10^2 - 1 + assert_eq!(macros::is_power_of_ten!(101u128), false); // 10^2 + 1 + assert_eq!(macros::is_power_of_ten!(99999999999999999999999999999999999999u128), false); // 10^38 - 1 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000001u128), false); // 10^38 + 1 } #[test] @@ -64,11 +72,21 @@ fun is_power_of_ten_returns_false_at_boundaries_u256() { assert_eq!(macros::is_power_of_ten!(11u256), false); assert_eq!(macros::is_power_of_ten!(99u256), false); assert_eq!(macros::is_power_of_ten!(101u256), false); - assert_eq!(macros::is_power_of_ten!(99999999999999999999999999999999999999999999999999999999999999999999999999999u256), false); // 10^77 - 1 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000000000000000000000000000000000000000001u256), false); // 10^77 + 1 -} - -// INV-R4: arbitrary non-powers. + assert_eq!( + macros::is_power_of_ten!( + 99999999999999999999999999999999999999999999999999999999999999999999999999999u256, + ), + false, + ); // 10^77 - 1 + assert_eq!( + macros::is_power_of_ten!( + 100000000000000000000000000000000000000000000000000000000000000000000000000001u256, + ), + false, + ); // 10^77 + 1 +} + +// Arbitrary non-powers. #[test] fun is_power_of_ten_returns_false_for_non_powers_u128() { assert_eq!(macros::is_power_of_ten!(2u128), false); @@ -83,7 +101,7 @@ fun is_power_of_ten_returns_false_for_non_powers_u256() { assert_eq!(macros::is_power_of_ten!(999u256), false); } -// INV-T1: macro is generic over `$Int` for any unsigned integer type. Production code +// The macro is generic over `$Int` for any unsigned integer type. Production code // only instantiates the macro at u128 and u256, but the macro body is intrinsically // type-generic — these tests pin down the contract at the smaller widths. #[test] @@ -123,19 +141,34 @@ fun is_power_of_ten_macro_works_on_u64() { assert_eq!(macros::is_power_of_ten!(std::u64::max_value!()), false); } -// INV-R3 + INV-R4 at the log10_floor cascade boundary k=64. +// log10_floor cascade boundary at k=64. // log10_floor's outermost dispatch branch is `if (value >= TEN_POW_64)`. No other test in // the package (neither is_power_of_ten nor log10's own test suite) exercises this exact // threshold. A regression in log10_floor's k=64 branch would silently skew the predicate // for every exponent at or above 64 without any existing test catching it. #[test] fun is_power_of_ten_at_log10_floor_cascade_boundary_u256() { - assert_eq!(macros::is_power_of_ten!(10000000000000000000000000000000000000000000000000000000000000000u256), true); // 10^64 - assert_eq!(macros::is_power_of_ten!(9999999999999999999999999999999999999999999999999999999999999999u256), false); // 10^64 - 1 - assert_eq!(macros::is_power_of_ten!(10000000000000000000000000000000000000000000000000000000000000001u256), false); // 10^64 + 1 -} - -// INV-R5 + INV-R6: TYPE_MAX is not a power of ten, and the macro must not abort. + assert_eq!( + macros::is_power_of_ten!( + 10000000000000000000000000000000000000000000000000000000000000000u256, + ), + true, + ); // 10^64 + assert_eq!( + macros::is_power_of_ten!( + 9999999999999999999999999999999999999999999999999999999999999999u256, + ), + false, + ); // 10^64 - 1 + assert_eq!( + macros::is_power_of_ten!( + 10000000000000000000000000000000000000000000000000000000000000001u256, + ), + false, + ); // 10^64 + 1 +} + +// TYPE_MAX is not a power of ten, and the macro must not abort. // For u256_max, log10_floor returns 77 — this exercises the largest possible exponent // and confirms std::u256::pow(10, 77) does not overflow. #[test] From 4393540ceebefaed749bab26411dec3419d5c8a1 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 19 May 2026 18:16:33 +0200 Subject: [PATCH 03/12] chore: drop CHANGELOG entry for is_power_of_ten refactor --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a74eede1..264f81c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - `sqrt` for `UD30x9` and `SD29x9` with round-down semantics. (#286) - `log2`, `ln`, `log10` for `UD30x9` (rounds down) and `SD29x9` (rounds toward zero). (#320) -### `openzeppelin_math` - -#### Changed - -- `u128::is_power_of_ten` and `u256::is_power_of_ten` now compute the result via `log10_floor` and `pow` instead of a hardcoded lookup table, eliminating the duplicated power-of-ten constants that previously caused the missing `10^77` entry (see #291). Public API unchanged. - ## 1.1.0 (21-04-2026) ### `openzeppelin_fp_math` From c73be742647896c271bf2f21c37dec260815233b Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 19 May 2026 18:21:49 +0200 Subject: [PATCH 04/12] ref: use receiver syntax for u256.pow in is_power_of_ten macro --- math/core/sources/internal/macros.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/math/core/sources/internal/macros.move b/math/core/sources/internal/macros.move index 33dec851..6100c56d 100644 --- a/math/core/sources/internal/macros.move +++ b/math/core/sources/internal/macros.move @@ -425,7 +425,7 @@ public(package) macro fun is_power_of_ten<$Int>($n: $Int): bool { return false }; let exp = log10_floor(n); - n == std::u256::pow(10, exp) + n == 10u256.pow(exp) } /// Compute floor(log10(value)) using binary search over powers of 10. From 2eeb7c627901a4a5887bbf2f79a6995fa57d5e42 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 19 May 2026 18:24:42 +0200 Subject: [PATCH 05/12] ref: apply suggestion from @0xNeshi Co-authored-by: Nenad --- math/core/sources/internal/macros.move | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/math/core/sources/internal/macros.move b/math/core/sources/internal/macros.move index 6100c56d..7576f50a 100644 --- a/math/core/sources/internal/macros.move +++ b/math/core/sources/internal/macros.move @@ -408,8 +408,7 @@ public(package) macro fun log10<$Int>($value: $Int, $rounding_mode: RoundingMode /// `k >= 0`). /// /// The macro widens the input to `u256`, short-circuits to `false` on zero, then verifies -/// `n == 10^log10_floor(n)`. Mirrors the structure of `log10` above: widen → early-return -/// on zero → let-bound `log10_floor` result → equality check via `std::u256::pow`. +/// `n == 10^log10_floor(n)`. /// /// #### Generics /// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`). From afb3f480361e78cbb65ed66ad55f9387b0cfa19e Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 19 May 2026 18:26:13 +0200 Subject: [PATCH 06/12] fix: fmt --- math/core/sources/internal/macros.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/math/core/sources/internal/macros.move b/math/core/sources/internal/macros.move index 7576f50a..94129b45 100644 --- a/math/core/sources/internal/macros.move +++ b/math/core/sources/internal/macros.move @@ -408,7 +408,7 @@ public(package) macro fun log10<$Int>($value: $Int, $rounding_mode: RoundingMode /// `k >= 0`). /// /// The macro widens the input to `u256`, short-circuits to `false` on zero, then verifies -/// `n == 10^log10_floor(n)`. +/// `n == 10^log10_floor(n)`. /// /// #### Generics /// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`). From 3ba494b9177d5c8887debf2a4e1a5df86f26e2eb Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Wed, 20 May 2026 08:41:08 +0200 Subject: [PATCH 07/12] docs: update docs --- CHANGELOG.md | 6 ++++++ math/core/sources/internal/macros.move | 3 --- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 264f81c7..8569cc3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - `sqrt` for `UD30x9` and `SD29x9` with round-down semantics. (#286) - `log2`, `ln`, `log10` for `UD30x9` (rounds down) and `SD29x9` (rounds toward zero). (#320) +### `openzeppelin_math` + +#### Changed + +- `u128::is_power_of_ten` and `u256::is_power_of_ten` now compute the result via `log10_floor` and `pow` instead of a hardcoded lookup table. (#323) + ## 1.1.0 (21-04-2026) ### `openzeppelin_fp_math` diff --git a/math/core/sources/internal/macros.move b/math/core/sources/internal/macros.move index 94129b45..f9053e48 100644 --- a/math/core/sources/internal/macros.move +++ b/math/core/sources/internal/macros.move @@ -407,9 +407,6 @@ public(package) macro fun log10<$Int>($value: $Int, $rounding_mode: RoundingMode /// Test whether `$n` is an exact non-negative integer power of ten (`10^k` for some /// `k >= 0`). /// -/// The macro widens the input to `u256`, short-circuits to `false` on zero, then verifies -/// `n == 10^log10_floor(n)`. -/// /// #### Generics /// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`). /// From 443ee29fdf1f4b5e2568d5693823925ed23a13f7 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Wed, 20 May 2026 08:52:39 +0200 Subject: [PATCH 08/12] chore: test cleanup --- math/core/tests/macros/is_power_of_ten.move | 113 ++------------------ 1 file changed, 6 insertions(+), 107 deletions(-) diff --git a/math/core/tests/macros/is_power_of_ten.move b/math/core/tests/macros/is_power_of_ten.move index f10554c0..ca409d15 100644 --- a/math/core/tests/macros/is_power_of_ten.move +++ b/math/core/tests/macros/is_power_of_ten.move @@ -4,106 +4,10 @@ module openzeppelin_math::is_power_of_ten; use openzeppelin_math::macros; use std::unit_test::assert_eq; -// Explicit zero guard returns false on u128 and u256 instantiations. -#[test] -fun is_power_of_ten_returns_false_for_zero_u128() { - assert_eq!(macros::is_power_of_ten!(0u128), false); -} - -#[test] -fun is_power_of_ten_returns_false_for_zero_u256() { - assert_eq!(macros::is_power_of_ten!(0u256), false); -} - -// 10^0 = 1 is a power of ten. -#[test] -fun is_power_of_ten_returns_true_for_one_u128() { - assert_eq!(macros::is_power_of_ten!(1u128), true); -} - -#[test] -fun is_power_of_ten_returns_true_for_one_u256() { - assert_eq!(macros::is_power_of_ten!(1u256), true); -} - -// Positive cases — sampled exponents covering low, mid, and u128 boundary. -#[test] -fun is_power_of_ten_returns_true_for_sampled_powers_u128() { - assert_eq!(macros::is_power_of_ten!(10u128), true); // 10^1 - assert_eq!(macros::is_power_of_ten!(100000u128), true); // 10^5 - assert_eq!(macros::is_power_of_ten!(10000000000000000000u128), true); // 10^19 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u128), true); // 10^38, u128 boundary -} - -// Positive cases — sampled exponents covering low, mid, and u256 boundary -// (including values beyond the u128 range to exercise generic widening at u256 width). -#[test] -fun is_power_of_ten_returns_true_for_sampled_powers_u256() { - assert_eq!(macros::is_power_of_ten!(10u256), true); // 10^1 - assert_eq!(macros::is_power_of_ten!(100000u256), true); // 10^5 - assert_eq!(macros::is_power_of_ten!(10000000000000000000u256), true); // 10^19 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u256), true); // 10^38 - assert_eq!( - macros::is_power_of_ten!(100000000000000000000000000000000000000000000000000u256), - true, - ); // 10^50 - assert_eq!( - macros::is_power_of_ten!( - 100000000000000000000000000000000000000000000000000000000000000000000000000000u256, - ), - true, - ); // 10^77, u256 boundary -} - -// Boundary negatives — values one below and one above sampled powers. -#[test] -fun is_power_of_ten_returns_false_at_boundaries_u128() { - assert_eq!(macros::is_power_of_ten!(9u128), false); // 10^1 - 1 - assert_eq!(macros::is_power_of_ten!(11u128), false); // 10^1 + 1 - assert_eq!(macros::is_power_of_ten!(99u128), false); // 10^2 - 1 - assert_eq!(macros::is_power_of_ten!(101u128), false); // 10^2 + 1 - assert_eq!(macros::is_power_of_ten!(99999999999999999999999999999999999999u128), false); // 10^38 - 1 - assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000001u128), false); // 10^38 + 1 -} - -#[test] -fun is_power_of_ten_returns_false_at_boundaries_u256() { - assert_eq!(macros::is_power_of_ten!(9u256), false); - assert_eq!(macros::is_power_of_ten!(11u256), false); - assert_eq!(macros::is_power_of_ten!(99u256), false); - assert_eq!(macros::is_power_of_ten!(101u256), false); - assert_eq!( - macros::is_power_of_ten!( - 99999999999999999999999999999999999999999999999999999999999999999999999999999u256, - ), - false, - ); // 10^77 - 1 - assert_eq!( - macros::is_power_of_ten!( - 100000000000000000000000000000000000000000000000000000000000000000000000000001u256, - ), - false, - ); // 10^77 + 1 -} - -// Arbitrary non-powers. -#[test] -fun is_power_of_ten_returns_false_for_non_powers_u128() { - assert_eq!(macros::is_power_of_ten!(2u128), false); - assert_eq!(macros::is_power_of_ten!(3u128), false); - assert_eq!(macros::is_power_of_ten!(999u128), false); -} - -#[test] -fun is_power_of_ten_returns_false_for_non_powers_u256() { - assert_eq!(macros::is_power_of_ten!(2u256), false); - assert_eq!(macros::is_power_of_ten!(3u256), false); - assert_eq!(macros::is_power_of_ten!(999u256), false); -} - // The macro is generic over `$Int` for any unsigned integer type. Production code -// only instantiates the macro at u128 and u256, but the macro body is intrinsically -// type-generic — these tests pin down the contract at the smaller widths. +// only instantiates the macro at u128 and u256 (covered indirectly by `u128_tests` +// and `u256_tests`); these tests pin down the contract at the smaller widths so a +// future routing change at u8–u64 cannot silently regress. #[test] fun is_power_of_ten_macro_works_on_u8() { assert_eq!(macros::is_power_of_ten!(0u8), false); @@ -168,14 +72,9 @@ fun is_power_of_ten_at_log10_floor_cascade_boundary_u256() { ); // 10^64 + 1 } -// TYPE_MAX is not a power of ten, and the macro must not abort. -// For u256_max, log10_floor returns 77 — this exercises the largest possible exponent -// and confirms std::u256::pow(10, 77) does not overflow. -#[test] -fun is_power_of_ten_returns_false_for_u128_max() { - assert_eq!(macros::is_power_of_ten!(std::u128::max_value!()), false); -} - +// u256_max exercises the largest possible exponent (log10_floor returns 77) and confirms +// `std::u256::pow(10, 77)` does not overflow — the unique INV-R6 enforcement point that +// `u256_tests::is_power_of_ten_*` does not cover directly. #[test] fun is_power_of_ten_returns_false_for_u256_max() { assert_eq!(macros::is_power_of_ten!(std::u256::max_value!()), false); From ffa471898bc7ff64a886bb23f22fb716a93771b5 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Wed, 20 May 2026 08:59:10 +0200 Subject: [PATCH 09/12] test: cleanup --- math/core/tests/macros/is_power_of_ten.move | 42 ++------------------- math/core/tests/u256_tests.move | 27 +++++++++++++ 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/math/core/tests/macros/is_power_of_ten.move b/math/core/tests/macros/is_power_of_ten.move index ca409d15..ee509ee1 100644 --- a/math/core/tests/macros/is_power_of_ten.move +++ b/math/core/tests/macros/is_power_of_ten.move @@ -4,10 +4,9 @@ module openzeppelin_math::is_power_of_ten; use openzeppelin_math::macros; use std::unit_test::assert_eq; -// The macro is generic over `$Int` for any unsigned integer type. Production code -// only instantiates the macro at u128 and u256 (covered indirectly by `u128_tests` -// and `u256_tests`); these tests pin down the contract at the smaller widths so a -// future routing change at u8–u64 cannot silently regress. +// Production code at u8–u64 does not route through this macro; these tests pin +// down the macro contract at those widths so a future routing change cannot +// silently regress. #[test] fun is_power_of_ten_macro_works_on_u8() { assert_eq!(macros::is_power_of_ten!(0u8), false); @@ -44,38 +43,3 @@ fun is_power_of_ten_macro_works_on_u64() { assert_eq!(macros::is_power_of_ten!(9999999999999999999u64), false); assert_eq!(macros::is_power_of_ten!(std::u64::max_value!()), false); } - -// log10_floor cascade boundary at k=64. -// log10_floor's outermost dispatch branch is `if (value >= TEN_POW_64)`. No other test in -// the package (neither is_power_of_ten nor log10's own test suite) exercises this exact -// threshold. A regression in log10_floor's k=64 branch would silently skew the predicate -// for every exponent at or above 64 without any existing test catching it. -#[test] -fun is_power_of_ten_at_log10_floor_cascade_boundary_u256() { - assert_eq!( - macros::is_power_of_ten!( - 10000000000000000000000000000000000000000000000000000000000000000u256, - ), - true, - ); // 10^64 - assert_eq!( - macros::is_power_of_ten!( - 9999999999999999999999999999999999999999999999999999999999999999u256, - ), - false, - ); // 10^64 - 1 - assert_eq!( - macros::is_power_of_ten!( - 10000000000000000000000000000000000000000000000000000000000000001u256, - ), - false, - ); // 10^64 + 1 -} - -// u256_max exercises the largest possible exponent (log10_floor returns 77) and confirms -// `std::u256::pow(10, 77)` does not overflow — the unique INV-R6 enforcement point that -// `u256_tests::is_power_of_ten_*` does not cover directly. -#[test] -fun is_power_of_ten_returns_false_for_u256_max() { - assert_eq!(macros::is_power_of_ten!(std::u256::max_value!()), false); -} diff --git a/math/core/tests/u256_tests.move b/math/core/tests/u256_tests.move index 1f2653ef..855cba14 100644 --- a/math/core/tests/u256_tests.move +++ b/math/core/tests/u256_tests.move @@ -935,3 +935,30 @@ fun is_power_of_ten_diverse_inputs() { assert_eq!(u256::is_power_of_ten(50000000000000000000), false); // Between 10^19 and 10^20 assert_eq!(u256::is_power_of_ten(500000000000000000000000000000), false); // Between 10^29 and 10^30 } + +#[test] +fun is_power_of_ten_at_log10_floor_cascade_boundary() { + assert_eq!( + u256::is_power_of_ten( + 10000000000000000000000000000000000000000000000000000000000000000, + ), + true, + ); // 10^64 + assert_eq!( + u256::is_power_of_ten( + 9999999999999999999999999999999999999999999999999999999999999999, + ), + false, + ); // 10^64 - 1 + assert_eq!( + u256::is_power_of_ten( + 10000000000000000000000000000000000000000000000000000000000000001, + ), + false, + ); // 10^64 + 1 +} + +#[test] +fun is_power_of_ten_u256_max() { + assert_eq!(u256::is_power_of_ten(std::u256::max_value!()), false); +} From f9249b7ccdfad05809b1aca0809ff7cd3dd82da6 Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 20 May 2026 11:00:20 +0200 Subject: [PATCH 10/12] Remove the orphaned "binary_search" internal helper macro and its tests --- math/core/sources/internal/macros.move | 38 ---- math/core/tests/macros/binary_search.move | 239 ---------------------- 2 files changed, 277 deletions(-) delete mode 100644 math/core/tests/macros/binary_search.move diff --git a/math/core/sources/internal/macros.move b/math/core/sources/internal/macros.move index f9053e48..3424330d 100644 --- a/math/core/sources/internal/macros.move +++ b/math/core/sources/internal/macros.move @@ -1035,41 +1035,3 @@ public(package) fun mul_mod_impl(a: u256, b: u256, modulus: u256): u256 { ((a * b) % modulus) } } - -/// Perform binary search on a sorted vector to find if `$needle` exists in the vector. -/// -/// The helper works across all unsigned widths and performs a standard iterative binary -/// search on a sorted vector. It compares the search value against the middle element -/// at each iteration until either the value is found or the search space is exhausted. -/// -/// #### Generics -/// - `$Int`: Any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, or `u256`). -/// -/// #### Parameters -/// - `$haystack`: A sorted vector of unsigned integers to search through. -/// - `$needle`: The value to search for in the vector. -/// -/// #### Returns -/// `true` if `$needle` exists in `$haystack`, `false` otherwise. -public(package) macro fun binary_search<$Int>($haystack: vector<$Int>, $needle: $Int): bool { - let haystack = $haystack; - let needle = $needle; - - let mut left = 0; - let mut right = haystack.length(); - - while (left < right) { - let mid = left + (right - left) / 2; - let mid_val = *haystack.borrow(mid); - - if (mid_val == needle) { - return true - } else if (mid_val < needle) { - left = mid + 1; - } else { - right = mid; - } - }; - - false -} diff --git a/math/core/tests/macros/binary_search.move b/math/core/tests/macros/binary_search.move deleted file mode 100644 index 741114aa..00000000 --- a/math/core/tests/macros/binary_search.move +++ /dev/null @@ -1,239 +0,0 @@ -#[test_only] -module openzeppelin_math::binary_search; - -use openzeppelin_math::macros; -use std::unit_test::assert_eq; - -#[test] -fun binary_search_finds_element_at_beginning() { - let haystack = vector[1u64, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - assert_eq!(macros::binary_search!(haystack, 1u64), true); -} - -#[test] -fun binary_search_finds_element_at_end() { - let haystack = vector[1u64, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - assert_eq!(macros::binary_search!(haystack, 10u64), true); -} - -#[test] -fun binary_search_finds_element_in_middle() { - let haystack = vector[1u64, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - assert_eq!(macros::binary_search!(haystack, 5u64), true); - assert_eq!(macros::binary_search!(haystack, 6u64), true); -} - -#[test] -fun binary_search_finds_all_elements() { - let haystack = vector[10u32, 20, 30, 40, 50]; - assert_eq!(macros::binary_search!(haystack, 10u32), true); - assert_eq!(macros::binary_search!(haystack, 20u32), true); - assert_eq!(macros::binary_search!(haystack, 30u32), true); - assert_eq!(macros::binary_search!(haystack, 40u32), true); - assert_eq!(macros::binary_search!(haystack, 50u32), true); -} - -#[test] -fun binary_search_returns_false_for_missing_element() { - let haystack = vector[1u64, 3, 5, 7, 9]; - assert_eq!(macros::binary_search!(haystack, 2u64), false); - assert_eq!(macros::binary_search!(haystack, 4u64), false); - assert_eq!(macros::binary_search!(haystack, 6u64), false); - assert_eq!(macros::binary_search!(haystack, 8u64), false); -} - -#[test] -fun binary_search_returns_false_for_value_below_range() { - let haystack = vector[10u64, 20, 30, 40, 50]; - assert_eq!(macros::binary_search!(haystack, 0u64), false); - assert_eq!(macros::binary_search!(haystack, 5u64), false); - assert_eq!(macros::binary_search!(haystack, 9u64), false); -} - -#[test] -fun binary_search_returns_false_for_value_above_range() { - let haystack = vector[10u64, 20, 30, 40, 50]; - assert_eq!(macros::binary_search!(haystack, 51u64), false); - assert_eq!(macros::binary_search!(haystack, 60u64), false); - assert_eq!(macros::binary_search!(haystack, 100u64), false); -} - -#[test] -fun binary_search_handles_empty_vector() { - let haystack = vector[]; - assert_eq!(macros::binary_search!(haystack, 0u64), false); - assert_eq!(macros::binary_search!(haystack, 1u64), false); -} - -#[test] -fun binary_search_handles_single_element_found() { - let haystack = vector[42u64]; - assert_eq!(macros::binary_search!(haystack, 42u64), true); -} - -#[test] -fun binary_search_handles_single_element_not_found() { - let haystack = vector[42u64]; - assert_eq!(macros::binary_search!(haystack, 41u64), false); - assert_eq!(macros::binary_search!(haystack, 43u64), false); -} - -#[test] -fun binary_search_handles_two_elements() { - let haystack = vector[10u64, 20]; - assert_eq!(macros::binary_search!(haystack, 10u64), true); - assert_eq!(macros::binary_search!(haystack, 20u64), true); - assert_eq!(macros::binary_search!(haystack, 5u64), false); - assert_eq!(macros::binary_search!(haystack, 15u64), false); - assert_eq!(macros::binary_search!(haystack, 25u64), false); -} - -#[test] -fun binary_search_handles_duplicates() { - let haystack = vector[1u64, 2, 2, 2, 3]; - assert_eq!(macros::binary_search!(haystack, 2u64), true); - assert_eq!(macros::binary_search!(haystack, 1u64), true); - assert_eq!(macros::binary_search!(haystack, 3u64), true); -} - -#[test] -fun binary_search_works_with_u8() { - let haystack = vector[1u8, 10, 20, 30, 40, 50, 100, 200, 255]; - assert_eq!(macros::binary_search!(haystack, 1u8), true); - assert_eq!(macros::binary_search!(haystack, 30u8), true); - assert_eq!(macros::binary_search!(haystack, 255u8), true); - assert_eq!(macros::binary_search!(haystack, 25u8), false); -} - -#[test] -fun binary_search_works_with_u16() { - let haystack = vector[100u16, 1000, 10000, 50000, 65535]; - assert_eq!(macros::binary_search!(haystack, 100u16), true); - assert_eq!(macros::binary_search!(haystack, 10000u16), true); - assert_eq!(macros::binary_search!(haystack, 65535u16), true); - assert_eq!(macros::binary_search!(haystack, 500u16), false); -} - -#[test] -fun binary_search_works_with_u32() { - let haystack = vector[1u32, 1000, 1000000, 1000000000]; - assert_eq!(macros::binary_search!(haystack, 1u32), true); - assert_eq!(macros::binary_search!(haystack, 1000000u32), true); - assert_eq!(macros::binary_search!(haystack, 999999u32), false); -} - -#[test] -fun binary_search_works_with_u128() { - let haystack = vector[ - 1u128, - 1000, - 1000000, - 1000000000, - 1000000000000, - 340282366920938463463374607431768211455, // max u128 - ]; - assert_eq!(macros::binary_search!(haystack, 1u128), true); - assert_eq!(macros::binary_search!(haystack, 1000000000000u128), true); - assert_eq!(macros::binary_search!(haystack, 340282366920938463463374607431768211455u128), true); - assert_eq!(macros::binary_search!(haystack, 999u128), false); -} - -#[test] -fun binary_search_works_with_u256() { - let haystack = vector[ - 1u256, - 1000, - 1000000, - 1000000000, - 1000000000000000000, - std::u256::max_value!(), - ]; - assert_eq!(macros::binary_search!(haystack, 1u256), true); - assert_eq!(macros::binary_search!(haystack, 1000000000u256), true); - assert_eq!(macros::binary_search!(haystack, std::u256::max_value!()), true); - assert_eq!(macros::binary_search!(haystack, 999u256), false); -} - -#[test] -fun binary_search_handles_large_sorted_vector() { - // Test with a larger vector to ensure binary search efficiency - let haystack = vector[ - 1u64, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - ]; - assert_eq!(macros::binary_search!(haystack, 1u64), true); - assert_eq!(macros::binary_search!(haystack, 15u64), true); - assert_eq!(macros::binary_search!(haystack, 30u64), true); - assert_eq!(macros::binary_search!(haystack, 16u64), true); - assert_eq!(macros::binary_search!(haystack, 0u64), false); - assert_eq!(macros::binary_search!(haystack, 31u64), false); -} - -#[test] -fun binary_search_handles_powers_of_ten() { - // Test with actual powers of 10 like in is_power_of_ten - let powers = vector[ - 1u64, - 10, - 100, - 1000, - 10000, - 100000, - 1000000, - 10000000, - 100000000, - 1000000000, - 10000000000, - 100000000000, - 1000000000000, - 10000000000000, - 100000000000000, - 1000000000000000, - 10000000000000000, - 100000000000000000, - 1000000000000000000, - 10000000000000000000, - ]; - - // All powers should be found - assert_eq!(macros::binary_search!(powers, 1u64), true); - assert_eq!(macros::binary_search!(powers, 10u64), true); - assert_eq!(macros::binary_search!(powers, 1000u64), true); - assert_eq!(macros::binary_search!(powers, 1000000u64), true); - assert_eq!(macros::binary_search!(powers, 10000000000000000000u64), true); - - // Non-powers should not be found - assert_eq!(macros::binary_search!(powers, 2u64), false); - assert_eq!(macros::binary_search!(powers, 11u64), false); - assert_eq!(macros::binary_search!(powers, 999u64), false); - assert_eq!(macros::binary_search!(powers, 1001u64), false); - assert_eq!(macros::binary_search!(powers, 9999u64), false); -} From d854e96778d885c1d6a5433de87b0268133398d2 Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 20 May 2026 11:50:49 +0200 Subject: [PATCH 11/12] Add more test cases --- math/core/tests/macros/is_power_of_ten.move | 94 ++++++++++++++++++++- math/core/tests/u128_tests.move | 5 ++ math/core/tests/u256_tests.move | 31 +++++++ 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/math/core/tests/macros/is_power_of_ten.move b/math/core/tests/macros/is_power_of_ten.move index ee509ee1..2b00d71b 100644 --- a/math/core/tests/macros/is_power_of_ten.move +++ b/math/core/tests/macros/is_power_of_ten.move @@ -4,8 +4,9 @@ module openzeppelin_math::is_power_of_ten; use openzeppelin_math::macros; use std::unit_test::assert_eq; -// Production code at u8–u64 does not route through this macro; these tests pin -// down the macro contract at those widths so a future routing change cannot +// Exhaustive macro-level coverage of `is_power_of_ten!` across every supported +// width. u128 and u256 are the production-routed widths; u8–u64 do not currently +// route through the macro but are tested here so a future routing change cannot // silently regress. #[test] fun is_power_of_ten_macro_works_on_u8() { @@ -43,3 +44,92 @@ fun is_power_of_ten_macro_works_on_u64() { assert_eq!(macros::is_power_of_ten!(9999999999999999999u64), false); assert_eq!(macros::is_power_of_ten!(std::u64::max_value!()), false); } + +#[test] +fun is_power_of_ten_macro_works_on_u128() { + // Zero is not a power of ten. + assert_eq!(macros::is_power_of_ten!(0u128), false); + + // Sampled powers across the u128 range (10^0 .. 10^38). + assert_eq!(macros::is_power_of_ten!(1u128), true); + assert_eq!(macros::is_power_of_ten!(10u128), true); + assert_eq!(macros::is_power_of_ten!(100u128), true); + assert_eq!(macros::is_power_of_ten!(10000u128), true); // 10^4 + assert_eq!(macros::is_power_of_ten!(100000000u128), true); // 10^8 + assert_eq!(macros::is_power_of_ten!(10000000000000000u128), true); // 10^16 + assert_eq!(macros::is_power_of_ten!(10000000000000000000u128), true); // 10^19 + assert_eq!( + macros::is_power_of_ten!(100000000000000000000000000000000u128), + true, + ); // 10^32 + assert_eq!( + macros::is_power_of_ten!(100000000000000000000000000000000000000u128), + true, + ); // 10^38 (max power of ten in u128) + + // Off-by-one non-powers around several powers. + assert_eq!(macros::is_power_of_ten!(9u128), false); + assert_eq!(macros::is_power_of_ten!(11u128), false); + assert_eq!(macros::is_power_of_ten!(99u128), false); + assert_eq!(macros::is_power_of_ten!(101u128), false); + assert_eq!( + macros::is_power_of_ten!(99999999999999999999999999999999999999u128), + false, + ); // 10^38 - 1 + + // Multiples of ten that are not powers of ten. + assert_eq!(macros::is_power_of_ten!(20u128), false); + assert_eq!(macros::is_power_of_ten!(500u128), false); + + // u128::MAX is not a power of ten. + assert_eq!(macros::is_power_of_ten!(std::u128::max_value!()), false); +} + +#[test] +fun is_power_of_ten_macro_works_on_u256() { + // Zero is not a power of ten. + assert_eq!(macros::is_power_of_ten!(0u256), false); + + // Sampled powers across the u256 range (10^0 .. 10^77). + assert_eq!(macros::is_power_of_ten!(1u256), true); + assert_eq!(macros::is_power_of_ten!(10u256), true); + assert_eq!(macros::is_power_of_ten!(100u256), true); + assert_eq!(macros::is_power_of_ten!(10000u256), true); // 10^4 + assert_eq!(macros::is_power_of_ten!(100000000u256), true); // 10^8 + assert_eq!(macros::is_power_of_ten!(10000000000000000u256), true); // 10^16 + assert_eq!( + macros::is_power_of_ten!(100000000000000000000000000000000u256), + true, + ); // 10^32 + assert_eq!( + macros::is_power_of_ten!( + 10000000000000000000000000000000000000000000000000000000000000000u256, + ), + true, + ); // 10^64 + assert_eq!( + macros::is_power_of_ten!( + 100000000000000000000000000000000000000000000000000000000000000000000000000000u256, + ), + true, + ); // 10^77 (max power of ten in u256) + + // Off-by-one non-powers around several powers. + assert_eq!(macros::is_power_of_ten!(9u256), false); + assert_eq!(macros::is_power_of_ten!(11u256), false); + assert_eq!(macros::is_power_of_ten!(99u256), false); + assert_eq!(macros::is_power_of_ten!(101u256), false); + assert_eq!( + macros::is_power_of_ten!( + 99999999999999999999999999999999999999999999999999999999999999999999999999999u256, + ), + false, + ); // 10^77 - 1 + + // Multiples of ten that are not powers of ten. + assert_eq!(macros::is_power_of_ten!(20u256), false); + assert_eq!(macros::is_power_of_ten!(500u256), false); + + // u256::MAX is not a power of ten. + assert_eq!(macros::is_power_of_ten!(std::u256::max_value!()), false); +} diff --git a/math/core/tests/u128_tests.move b/math/core/tests/u128_tests.move index 0fc26c71..0a0a8209 100644 --- a/math/core/tests/u128_tests.move +++ b/math/core/tests/u128_tests.move @@ -843,3 +843,8 @@ fun is_power_of_ten_diverse_inputs() { assert_eq!(u128::is_power_of_ten(50000000000000000000), false); // Between 10^19 and 10^20 assert_eq!(u128::is_power_of_ten(500000000000000000000000000000), false); // Between 10^29 and 10^30 } + +#[test] +fun is_power_of_ten_u128_max() { + assert_eq!(u128::is_power_of_ten(std::u128::max_value!()), false); +} diff --git a/math/core/tests/u256_tests.move b/math/core/tests/u256_tests.move index 855cba14..302278f6 100644 --- a/math/core/tests/u256_tests.move +++ b/math/core/tests/u256_tests.move @@ -938,6 +938,7 @@ fun is_power_of_ten_diverse_inputs() { #[test] fun is_power_of_ten_at_log10_floor_cascade_boundary() { + // 10^64 boundary (TEN_POW_64 cascade branch). assert_eq!( u256::is_power_of_ten( 10000000000000000000000000000000000000000000000000000000000000000, @@ -956,6 +957,36 @@ fun is_power_of_ten_at_log10_floor_cascade_boundary() { ), false, ); // 10^64 + 1 + + // 10^32 boundary (TEN_POW_32 cascade branch). + assert_eq!(u256::is_power_of_ten(100000000000000000000000000000000), true); // 10^32 + assert_eq!(u256::is_power_of_ten(99999999999999999999999999999999), false); // 10^32 - 1 + assert_eq!(u256::is_power_of_ten(100000000000000000000000000000001), false); // 10^32 + 1 + + // 10^16 boundary (TEN_POW_16 cascade branch). + assert_eq!(u256::is_power_of_ten(10000000000000000), true); // 10^16 + assert_eq!(u256::is_power_of_ten(9999999999999999), false); // 10^16 - 1 + assert_eq!(u256::is_power_of_ten(10000000000000001), false); // 10^16 + 1 + + // 10^8 boundary (TEN_POW_8 cascade branch). + assert_eq!(u256::is_power_of_ten(100000000), true); // 10^8 + assert_eq!(u256::is_power_of_ten(99999999), false); // 10^8 - 1 + assert_eq!(u256::is_power_of_ten(100000001), false); // 10^8 + 1 + + // 10^4 boundary (TEN_POW_4 cascade branch). + assert_eq!(u256::is_power_of_ten(10000), true); // 10^4 + assert_eq!(u256::is_power_of_ten(9999), false); // 10^4 - 1 + assert_eq!(u256::is_power_of_ten(10001), false); // 10^4 + 1 + + // 10^2 boundary (TEN_POW_2 cascade branch). + assert_eq!(u256::is_power_of_ten(100), true); // 10^2 + assert_eq!(u256::is_power_of_ten(99), false); // 10^2 - 1 + assert_eq!(u256::is_power_of_ten(101), false); // 10^2 + 1 + + // 10 boundary (`value >= 10` cascade branch). + assert_eq!(u256::is_power_of_ten(10), true); // 10 + assert_eq!(u256::is_power_of_ten(9), false); // 10 - 1 + assert_eq!(u256::is_power_of_ten(11), false); // 10 + 1 } #[test] From 586c54217b02291c094c68c363de8243374666a7 Mon Sep 17 00:00:00 2001 From: immrsd Date: Wed, 20 May 2026 11:51:41 +0200 Subject: [PATCH 12/12] Format files --- math/core/tests/macros/is_power_of_ten.move | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/math/core/tests/macros/is_power_of_ten.move b/math/core/tests/macros/is_power_of_ten.move index 2b00d71b..be536046 100644 --- a/math/core/tests/macros/is_power_of_ten.move +++ b/math/core/tests/macros/is_power_of_ten.move @@ -58,24 +58,15 @@ fun is_power_of_ten_macro_works_on_u128() { assert_eq!(macros::is_power_of_ten!(100000000u128), true); // 10^8 assert_eq!(macros::is_power_of_ten!(10000000000000000u128), true); // 10^16 assert_eq!(macros::is_power_of_ten!(10000000000000000000u128), true); // 10^19 - assert_eq!( - macros::is_power_of_ten!(100000000000000000000000000000000u128), - true, - ); // 10^32 - assert_eq!( - macros::is_power_of_ten!(100000000000000000000000000000000000000u128), - true, - ); // 10^38 (max power of ten in u128) + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000u128), true); // 10^32 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000000000u128), true); // 10^38 (max power of ten in u128) // Off-by-one non-powers around several powers. assert_eq!(macros::is_power_of_ten!(9u128), false); assert_eq!(macros::is_power_of_ten!(11u128), false); assert_eq!(macros::is_power_of_ten!(99u128), false); assert_eq!(macros::is_power_of_ten!(101u128), false); - assert_eq!( - macros::is_power_of_ten!(99999999999999999999999999999999999999u128), - false, - ); // 10^38 - 1 + assert_eq!(macros::is_power_of_ten!(99999999999999999999999999999999999999u128), false); // 10^38 - 1 // Multiples of ten that are not powers of ten. assert_eq!(macros::is_power_of_ten!(20u128), false); @@ -97,10 +88,7 @@ fun is_power_of_ten_macro_works_on_u256() { assert_eq!(macros::is_power_of_ten!(10000u256), true); // 10^4 assert_eq!(macros::is_power_of_ten!(100000000u256), true); // 10^8 assert_eq!(macros::is_power_of_ten!(10000000000000000u256), true); // 10^16 - assert_eq!( - macros::is_power_of_ten!(100000000000000000000000000000000u256), - true, - ); // 10^32 + assert_eq!(macros::is_power_of_ten!(100000000000000000000000000000000u256), true); // 10^32 assert_eq!( macros::is_power_of_ten!( 10000000000000000000000000000000000000000000000000000000000000000u256,