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
23 changes: 21 additions & 2 deletions crates/float/abi/DecimalFloat.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions crates/float/src/js_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,31 @@ impl Float {
self.abs()
}

/// Returns the canonical representative of the float's numeric value.
///
/// Floats are non-canonical by design: multiple representations encode the
/// same number. `canonicalize` returns the single representative whose
/// magnitude is maximised within the type bounds, so two Floats are
/// numerically equal iff their canonical forms are byte-equal. Intended for
/// raw-byte equality use cases (map keys, hashing, set membership).
///
/// # Returns
///
/// * `Ok(Float)` - The canonical form.
/// * `Err(FloatError)` - If the operation fails.
///
/// # Example
///
/// ```typescript
/// const a = Float.parse("5").value!;
/// const b = Float.parse("5.0").value!;
/// assert(a.canonicalize().value.asHex() === b.canonicalize().value.asHex());
/// ```
#[wasm_export(js_name = "canonicalize", preserve_js_class)]
pub fn canonicalize_js(&self) -> Result<Float, FloatError> {
self.canonicalize()
}

/// Adds two floats.
///
/// # Returns
Expand Down
35 changes: 35 additions & 0 deletions crates/float/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,41 @@ impl Float {
})
}

/// Returns the canonical representative of the float's numeric value.
///
/// Floats are non-canonical by design: multiple `(coefficient, exponent)`
/// pairs encode the same number. `canonicalize` returns the single
/// representative whose magnitude is maximised within the type bounds, so
/// two Floats are numerically equal iff their canonical forms are
/// byte-equal. Intended for raw-byte equality use cases (map keys, hashing,
/// set membership, content-addressed storage).
///
/// # Returns
///
/// * `Ok(Float)` - The canonical form.
/// * `Err(FloatError)` - If the operation fails.
///
/// # Example
///
/// ```
/// use rain_math_float::Float;
///
/// let a = Float::parse("5".to_string())?;
/// let b = Float::parse("5.0".to_string())?;
/// assert_eq!(a.canonicalize()?.as_hex(), b.canonicalize()?.as_hex());
///
/// anyhow::Ok(())
/// ```
pub fn canonicalize(self) -> Result<Float, FloatError> {
let Float(a) = self;
let calldata = DecimalFloat::canonicalizeCall { a }.abi_encode();

execute_call(Bytes::from(calldata), |output| {
let decoded = DecimalFloat::canonicalizeCall::abi_decode_returns(output.as_ref())?;
Ok(Float(decoded))
})
}

/// Returns `true` if `self` is less than or equal to `b`.
///
/// # Arguments
Expand Down
7 changes: 7 additions & 0 deletions src/concrete/DecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ contract DecimalFloat {
return a.isZero();
}

/// Exposes `LibDecimalFloat.canonicalize` for offchain use.
/// @param a The float to canonicalize.
/// @return The canonical representative of the float's numeric value.
function canonicalize(Float a) external pure returns (Float) {
return a.canonicalize();
}

/// Exposes `LibDecimalFloat.fromFixedDecimalLosslessPacked` for offchain
/// use.
/// @param value The fixed point decimal value to convert.
Expand Down
40 changes: 40 additions & 0 deletions src/lib/LibDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,46 @@ library LibDecimalFloat {
}
}

/// Canonicalize a Float to a unique byte representation per numeric value.
/// Floats are non-canonical by design (see the docstring on the `Float`
/// type): multiple `(coefficient, exponent)` pairs encode the same number
/// and equality is numeric (`eq`) rather than byte-level. This function
/// returns the single representative whose magnitude-maximised packing is
/// stable, so two Floats are numerically equal iff their canonical forms
/// are byte-equal (`Float.unwrap(a.canonicalize()) == Float.unwrap(b.canonicalize())`).
/// Intended for consumers that need raw-byte equality: `mapping(Float => X)`
/// keys, hashing, set membership, content-addressed storage.
///
/// The chosen representative has the largest `|coefficient|` that fits
/// int224 subject to the exponent staying `>= type(int32).min`, reached by
/// scaling the coefficient up by ten directly within those bounds. This
/// never reverts for any valid input Float: scaling simply stops at the
/// limit. `canonicalize` is idempotent and value-preserving (the result is
/// `eq` to the input).
/// @param float The float to canonicalize.
/// @return The canonical representative of the float's numeric value.
function canonicalize(Float float) internal pure returns (Float) {
(int256 signedCoefficient, int256 exponent) = float.unpack();
if (signedCoefficient == 0) {
return FLOAT_ZERO;
}
unchecked {
while (exponent > type(int32).min) {
int256 trySignedCoefficient = signedCoefficient * 10;
// int224 overflow is the termination condition for the scaling
// loop, not a bug. The cast back is compared against the
// pre-cast value to detect that overflow.
// forge-lint: disable-next-line(unsafe-typecast)
if (int224(trySignedCoefficient) != trySignedCoefficient) {
break;
}
signedCoefficient = trySignedCoefficient;
exponent -= 1;
}
}
return packLossless(signedCoefficient, exponent);
}

/// Same as add, but accepts a Float struct instead of separate values.
/// Costs more gas but helps mitigate stack depth issues, and is more
/// ergonomic for the caller.
Expand Down
4 changes: 2 additions & 2 deletions src/lib/deploy/LibDecimalFloatDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ library LibDecimalFloatDeploy {
/// @dev Address of the DecimalFloat contract deployed via Zoltu's
/// deterministic deployment proxy.
/// This address is the same across all EVM-compatible networks.
address constant ZOLTU_DEPLOYED_DECIMAL_FLOAT_ADDRESS = address(0xBee0eEFaffD046c9602109eB30A858Be301CC926);
address constant ZOLTU_DEPLOYED_DECIMAL_FLOAT_ADDRESS = address(0x6b7C246F02E67299b5801f8215d7f40abD82056d);

/// @dev The expected codehash of the DecimalFloat contract deployed via
/// Zoltu's deterministic deployment proxy.
bytes32 constant DECIMAL_FLOAT_CONTRACT_HASH = 0x7a93d0311f7782b44157ba40e94ec936085ebe001c7893bdd74911c8351d3def;
bytes32 constant DECIMAL_FLOAT_CONTRACT_HASH = 0x61bbc303586dc1b233644acdebe38dcc757907d7b39edcf6b1152c2081cf3197;

/// Combines all log and anti-log tables into a single bytes array for
/// deployment. These are using packed encoding to minimize size and remove
Expand Down
31 changes: 31 additions & 0 deletions test/src/concrete/DecimalFloat.canonicalize.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol";
import {LogTest} from "test/abstract/LogTest.sol";
import {DecimalFloat} from "src/concrete/DecimalFloat.sol";

contract DecimalFloatCanonicalizeTest is LogTest {
using LibDecimalFloat for Float;

function canonicalizeExternal(Float a) external pure returns (Float) {
return a.canonicalize();
}

/// The `canonicalize` method exposed on the deployed concrete contract must
/// match the library result for any input (and revert identically if the
/// library reverts).
function testCanonicalizeDeployed(Float a) external {
DecimalFloat deployed = new DecimalFloat();

try this.canonicalizeExternal(a) returns (Float b) {
Float deployedB = deployed.canonicalize(a);

assertEq(Float.unwrap(b), Float.unwrap(deployedB));
} catch (bytes memory err) {
vm.expectRevert(err);
deployed.canonicalize(a);
}
}
}
Loading
Loading