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
14 changes: 11 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,19 @@ rain.deploy, rain.sol.codegen.
## Key Design Details

- 512-bit intermediate values in multiply/divide to preserve precision.
- Exponent underflow silently rounds toward zero; exponent overflow reverts.
- Exponent overflow and underflow both revert from the public arithmetic surface
(`ExponentOverflow` / `ExponentUnderflow`). Coefficient truncation on values
too large for int224 is silently tolerated because it preserves the order of
magnitude.
- Log/power use lookup table approximations with linear interpolation (table
deployed as a data contract).
- Two packing modes: lossless (reverts on precision loss) and lossy (returns
bool flag).
- Three packing modes:
- `packLossless`: reverts on any precision loss.
- `packLossy`: surfaces the `lossless` flag, returns `FLOAT_ZERO` on exponent
underflow. Used by parsing where underflow → "value rounds to zero" is a
legitimate parse result reported via `ParseDecimalPrecisionLoss`.
- `packArithmeticResult`: tolerates coefficient truncation, reverts on
exponent underflow. Used by every public arithmetic operation.
- Solidity compiler: 0.8.25, EVM target: Cancun, optimizer: 1,000,000 runs.

## License
Expand Down
20 changes: 18 additions & 2 deletions crates/float/abi/DecimalFloat.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/float/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub enum FloatError {
pub enum DecimalFloatErrorSelector {
CoefficientOverflow,
ExponentOverflow,
ExponentUnderflow,
Log10Negative,
Log10Zero,
LossyConversionFromFloat,
Expand All @@ -59,6 +60,7 @@ impl TryFrom<FixedBytes<4>> for DecimalFloatErrorSelector {
Ok(Self::CoefficientOverflow)
}
<DecimalFloat::ExponentOverflow as SolError>::SELECTOR => Ok(Self::ExponentOverflow),
<DecimalFloat::ExponentUnderflow as SolError>::SELECTOR => Ok(Self::ExponentUnderflow),
<DecimalFloat::Log10Negative as SolError>::SELECTOR => Ok(Self::Log10Negative),
<DecimalFloat::Log10Zero as SolError>::SELECTOR => Ok(Self::Log10Zero),
<DecimalFloat::LossyConversionFromFloat as SolError>::SELECTOR => {
Expand Down
11 changes: 8 additions & 3 deletions crates/float/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1780,14 +1780,19 @@ mod tests {
));
}

/// Multiplying near-min exponents underflows to zero.
/// Multiplying near-min exponents underflows; the public arithmetic
/// surface reverts with `ExponentUnderflow` rather than silently
/// producing zero.
#[test]
fn test_mul_exponent_underflow_error() {
let near_min_exp = Float::parse("1e-2147483646".to_string()).unwrap();
let one_e_neg_three = Float::parse("1e-3".to_string()).unwrap();

let float = (near_min_exp * one_e_neg_three).unwrap();
assert!(float.is_zero().unwrap());
let err = (near_min_exp * one_e_neg_three).unwrap_err();
assert!(matches!(
err,
FloatError::DecimalFloat(DecimalFloatErrors::ExponentUnderflow(_))
));
}

/// from_fixed_decimal for known value/decimals pairs matches parsed strings.
Expand Down
7 changes: 7 additions & 0 deletions src/error/ErrDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ error CoefficientOverflow(int256 signedCoefficient, int256 exponent);
/// @dev Thrown when an exponent overflows.
error ExponentOverflow(int256 signedCoefficient, int256 exponent);

/// @dev Thrown when an exponent underflows. Exponent underflow means the
/// magnitude is smaller than any representable Float. Without this revert,
/// arithmetic ops that compose to underflow (e.g. `mul` with two operands
/// whose exponents sum below `int32.min`) would silently return `FLOAT_ZERO`,
/// breaking downstream code that branches on `result == 0`.
error ExponentUnderflow(int256 signedCoefficient, int256 exponent);

/// @dev Thrown when attempting to convert a negative number to an unsigned
/// fixed-point number.
error NegativeFixedDecimalConversion(int256 signedCoefficient, int256 exponent);
Expand Down
46 changes: 32 additions & 14 deletions src/lib/LibDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.25;

import {
ExponentOverflow,
ExponentUnderflow,
CoefficientOverflow,
NegativeFixedDecimalConversion,
LossyConversionFromFloat,
Expand Down Expand Up @@ -380,6 +381,23 @@ library LibDecimalFloat {
return c;
}

/// Variant of `packLossy` used as the finaliser of every arithmetic
/// operation. Tolerates coefficient truncation (which preserves the order
/// of magnitude) but reverts on exponent underflow (which silently
/// replaces the value by `FLOAT_ZERO`, losing the magnitude entirely).
/// Distinguishes the two `lossless = false` modes from `packLossy` by the
/// returned float: `packLossy` only returns `FLOAT_ZERO` for the underflow
/// case when `lossless` is false (the coefficient-truncation path
/// successively divides by ten and never reaches zero from a non-zero
/// input).
function packArithmeticResult(int256 signedCoefficient, int256 exponent) internal pure returns (Float) {
(Float c, bool lossless) = packLossy(signedCoefficient, exponent);
if (!lossless && Float.unwrap(c) == bytes32(0)) {
revert ExponentUnderflow(signedCoefficient, exponent);
}
return c;
}

/// Unpack a packed bytes32 into a signed coefficient and exponent. This is
/// the inverse of `pack`.
/// @param float The packed representation of the signed coefficient and
Expand Down Expand Up @@ -410,7 +428,7 @@ library LibDecimalFloat {
LibDecimalFloatImplementation.add(signedCoefficientA, exponentA, signedCoefficientB, exponentB);
// Addition can be lossy.

(Float c,) = packLossy(signedCoefficient, exponent);
Float c = packArithmeticResult(signedCoefficient, exponent);
return c;
}

Expand All @@ -428,7 +446,7 @@ library LibDecimalFloat {
LibDecimalFloatImplementation.sub(signedCoefficientA, exponentA, signedCoefficientB, exponentB);
// Subtraction can be lossy.

(Float c,) = packLossy(signedCoefficientC, exponentC);
Float c = packArithmeticResult(signedCoefficientC, exponentC);
return c;
}

Expand All @@ -443,7 +461,7 @@ library LibDecimalFloat {
(signedCoefficient, exponent) = LibDecimalFloatImplementation.minus(signedCoefficient, exponent);
// Minus is a lossy operation due to the asymmetry of signed integers.

(Float result,) = packLossy(signedCoefficient, exponent);
Float result = packArithmeticResult(signedCoefficient, exponent);
return result;
}

Expand All @@ -467,7 +485,7 @@ library LibDecimalFloat {

// At the limit of signed values there is the potential for a lossy
// conversion when negating.
(Float result,) = packLossy(signedCoefficient, exponent);
Float result = packArithmeticResult(signedCoefficient, exponent);
return result;
}

Expand Down Expand Up @@ -499,7 +517,7 @@ library LibDecimalFloat {
(int256 signedCoefficient, int256 exponent) =
LibDecimalFloatImplementation.mul(signedCoefficientA, exponentA, signedCoefficientB, exponentB);
// Multiplication is typically lossless, but can be lossy in edge cases.
(Float c,) = packLossy(signedCoefficient, exponent);
Float c = packArithmeticResult(signedCoefficient, exponent);
return c;
}

Expand All @@ -518,7 +536,7 @@ library LibDecimalFloat {
LibDecimalFloatImplementation.div(signedCoefficientA, exponentA, signedCoefficientB, exponentB);
// Division is often lossy because it is very easy to end up with
// infinite decimal representations.
(Float c,) = packLossy(signedCoefficient, exponent);
Float c = packArithmeticResult(signedCoefficient, exponent);
return c;
}

Expand All @@ -531,7 +549,7 @@ library LibDecimalFloat {
function inv(Float float) internal pure returns (Float) {
(int256 signedCoefficient, int256 exponent) = float.unpack();
(signedCoefficient, exponent) = LibDecimalFloatImplementation.inv(signedCoefficient, exponent);
(Float result,) = packLossy(signedCoefficient, exponent);
Float result = packArithmeticResult(signedCoefficient, exponent);
return result;
}

Expand Down Expand Up @@ -615,7 +633,7 @@ library LibDecimalFloat {
(int256 signedCoefficient, int256 exponent) = float.unpack();
//slither-disable-next-line unused-return
(int256 i,) = LibDecimalFloatImplementation.intFrac(signedCoefficient, exponent);
(Float result,) = packLossy(i, exponent);
Float result = packArithmeticResult(i, exponent);
return result;
}

Expand All @@ -626,7 +644,7 @@ library LibDecimalFloat {
(int256 signedCoefficient, int256 exponent) = float.unpack();
//slither-disable-next-line unused-return
(, int256 fraction) = LibDecimalFloatImplementation.intFrac(signedCoefficient, exponent);
(Float result,) = packLossy(fraction, exponent);
Float result = packArithmeticResult(fraction, exponent);
return result;
}

Expand All @@ -645,7 +663,7 @@ library LibDecimalFloat {
// subtract 1 from the characteristic to floor it.
(i, exponent) = LibDecimalFloatImplementation.sub(i, exponent, 1e76, -76);
}
(Float result,) = packLossy(i, exponent);
Float result = packArithmeticResult(i, exponent);
return result;
}

Expand All @@ -672,7 +690,7 @@ library LibDecimalFloat {
(i, exponent) = LibDecimalFloatImplementation.add(i, exponent, 1e76, -76);
}

(Float result,) = packLossy(i, exponent);
Float result = packArithmeticResult(i, exponent);
return result;
}

Expand All @@ -690,7 +708,7 @@ library LibDecimalFloat {
LibDecimalFloatImplementation.pow10(tablesDataContract, signedCoefficient, exponent);
// We don't care if power10 is lossy because it's an approximation
// anyway.
(Float result,) = packLossy(signedCoefficient, exponent);
Float result = packArithmeticResult(signedCoefficient, exponent);
return result;
}

Expand All @@ -706,7 +724,7 @@ library LibDecimalFloat {
(signedCoefficient, exponent) =
LibDecimalFloatImplementation.log10(tablesDataContract, signedCoefficient, exponent);
// We don't care if log10 is lossy because it's an approximation anyway.
(Float result,) = packLossy(signedCoefficient, exponent);
Float result = packArithmeticResult(signedCoefficient, exponent);
return result;
}

Expand Down Expand Up @@ -785,7 +803,7 @@ library LibDecimalFloat {
(signedCoefficientC, exponentC) =
LibDecimalFloatImplementation.mul(signedCoefficientC, exponentC, signedCoefficientResult, exponentResult);
// We don't care if power is lossy because it's an approximation anyway.
(Float c,) = packLossy(signedCoefficientC, exponentC);
Float c = packArithmeticResult(signedCoefficientC, exponentC);
return c;
}

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(0xc08C2137eD976fCFF68cBFa847e73017EDB8fB47);
address constant ZOLTU_DEPLOYED_DECIMAL_FLOAT_ADDRESS = address(0x588F097a34D611D358c923087cBA5CB75165336A);

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

/// 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
3 changes: 1 addition & 2 deletions test/src/lib/LibDecimalFloat.add.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ contract LibDecimalFloatDecimalAddTest is Test {
{
(int256 signedCoefficientC, int256 exponentC) =
LibDecimalFloatImplementation.add(signedCoefficientA, exponentA, signedCoefficientB, exponentB);
(Float c, bool lossless) = LibDecimalFloat.packLossy(signedCoefficientC, exponentC);
(lossless);
Float c = LibDecimalFloat.packArithmeticResult(signedCoefficientC, exponentC);
return c;
}

Expand Down
15 changes: 12 additions & 3 deletions test/src/lib/LibDecimalFloat.div.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol";
import {LibDecimalFloat, Float, ExponentUnderflow} from "src/lib/LibDecimalFloat.sol";
import {LibDecimalFloatImplementation} from "src/lib/implementation/LibDecimalFloatImplementation.sol";

import {Test} from "forge-std-1.16.1/src/Test.sol";
Expand All @@ -17,15 +17,24 @@ contract LibDecimalFloatDivTest is Test {
{
(int256 signedCoefficientC, int256 exponentC) =
LibDecimalFloatImplementation.div(signedCoefficientA, exponentA, signedCoefficientB, exponentB);
(Float c, bool lossless) = LibDecimalFloat.packLossy(signedCoefficientC, exponentC);
(lossless);
Float c = LibDecimalFloat.packArithmeticResult(signedCoefficientC, exponentC);
return c;
}

function divExternal(Float floatA, Float floatB) external pure returns (Float) {
return LibDecimalFloat.div(floatA, floatB);
}

/// `div` whose result exponent (`expA - expB`) falls below `int32.min`
/// reverts instead of silently producing `FLOAT_ZERO`. Constructed by
/// numerator at the minimum exponent and denominator at the maximum.
function testDivRevertsOnExponentUnderflow() external {
Float a = LibDecimalFloat.packLossless(1, type(int32).min);
Float b = LibDecimalFloat.packLossless(1, type(int32).max);
vm.expectPartialRevert(ExponentUnderflow.selector);
this.divExternal(a, b);
}

function testDivPacked(Float a, Float b) external {
(int256 signedCoefficientA, int256 exponentA) = a.unpack();
(int256 signedCoefficientB, int256 exponentB) = b.unpack();
Expand Down
13 changes: 10 additions & 3 deletions test/src/lib/LibDecimalFloat.inv.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,30 @@
pragma solidity =0.8.25;

import {Test} from "forge-std-1.16.1/src/Test.sol";
import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol";
import {LibDecimalFloat, Float, ExponentUnderflow} from "src/lib/LibDecimalFloat.sol";
import {LibDecimalFloatImplementation} from "src/lib/implementation/LibDecimalFloatImplementation.sol";

contract LibDecimalFloatInvTest is Test {
using LibDecimalFloat for Float;

function invExternal(int256 signedCoefficient, int256 exponent) external pure returns (Float) {
(signedCoefficient, exponent) = LibDecimalFloatImplementation.inv(signedCoefficient, exponent);
(Float float, bool lossless) = LibDecimalFloat.packLossy(signedCoefficient, exponent);
(lossless);
Float float = LibDecimalFloat.packArithmeticResult(signedCoefficient, exponent);
return float;
}

function invExternal(Float float) external pure returns (Float) {
return LibDecimalFloat.inv(float);
}

/// `inv` of a Float whose representable inverse magnitude falls below
/// `int32.min` reverts instead of silently producing `FLOAT_ZERO`.
function testInvRevertsOnExponentUnderflow() external {
Float float = Float.wrap(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
vm.expectPartialRevert(ExponentUnderflow.selector);
this.invExternal(float);
}
Comment on lines +22 to +28
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Optional: decode the adversarial Float in a comment.

This literal corresponds to coefficient -1 (int224 all-ones) at exponent int32.max; making that explicit avoids forcing readers to hand-parse the bit layout.

📝 Suggested annotation
     function testInvRevertsOnExponentUnderflow() external {
+        // coefficient = -1 (int224, all ones), exponent = int32.max.
+        // 1 / (−1 × 10^int32.max) requires scaling that drops exponent below int32.min.
         Float float = Float.wrap(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
         vm.expectPartialRevert(ExponentUnderflow.selector);
         this.invExternal(float);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// `inv` of a Float whose representable inverse magnitude falls below
/// `int32.min` reverts instead of silently producing `FLOAT_ZERO`.
function testInvRevertsOnExponentUnderflow() external {
Float float = Float.wrap(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
vm.expectPartialRevert(ExponentUnderflow.selector);
this.invExternal(float);
}
/// `inv` of a Float whose representable inverse magnitude falls below
/// `int32.min` reverts instead of silently producing `FLOAT_ZERO`.
function testInvRevertsOnExponentUnderflow() external {
// coefficient = -1 (int224, all ones), exponent = int32.max.
// 1 / (−1 × 10^int32.max) requires scaling that drops exponent below int32.min.
Float float = Float.wrap(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
vm.expectPartialRevert(ExponentUnderflow.selector);
this.invExternal(float);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/src/lib/LibDecimalFloat.inv.t.sol` around lines 22 - 28, Add an inline
comment decoding the adversarial Float literal used in
testInvRevertsOnExponentUnderflow: explain that Float.wrap(0x7fff...ffff)
encodes coefficient = -1 (int224 all-ones) and exponent = int32.max, so the
value is -1 * 2^int32.max; place this comment next to the Float.wrap call (or
above the test) to clarify the bit layout for readers and maintainers
referencing the testInvRevertsOnExponentUnderflow and invExternal usage.


function testInvMem(Float float) external {
(int256 signedCoefficient, int256 exponent) = float.unpack();
try this.invExternal(signedCoefficient, exponent) returns (Float floatParts) {
Expand Down
16 changes: 13 additions & 3 deletions test/src/lib/LibDecimalFloat.mul.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol";
import {LibDecimalFloat, Float, ExponentUnderflow} from "src/lib/LibDecimalFloat.sol";
import {LibDecimalFloatImplementation} from "src/lib/implementation/LibDecimalFloatImplementation.sol";

import {Test} from "forge-std-1.16.1/src/Test.sol";
Expand All @@ -17,15 +17,25 @@ contract LibDecimalFloatMulTest is Test {
{
(int256 signedCoefficientC, int256 exponentC) =
LibDecimalFloatImplementation.mul(signedCoefficientA, exponentA, signedCoefficientB, exponentB);
(Float c, bool lossless) = LibDecimalFloat.packLossy(signedCoefficientC, exponentC);
(lossless);
Float c = LibDecimalFloat.packArithmeticResult(signedCoefficientC, exponentC);
return c;
}

function mulExternal(Float floatA, Float floatB) external pure returns (Float) {
return LibDecimalFloat.mul(floatA, floatB);
}

/// `mul` of two operands whose exponents sum below `int32.min` reverts
/// instead of silently producing `FLOAT_ZERO`. Without this, downstream
/// code that branches on `result == 0` would mistake a tiny magnitude
/// for an exact zero.
function testMulRevertsOnExponentUnderflow() external {
Float a = LibDecimalFloat.packLossless(1, type(int32).min);
Float b = LibDecimalFloat.packLossless(1, type(int32).min);
vm.expectPartialRevert(ExponentUnderflow.selector);
this.mulExternal(a, b);
}

function testMulPacked(Float a, Float b) external {
(int256 signedCoefficientA, int256 exponentA) = a.unpack();
(int256 signedCoefficientB, int256 exponentB) = b.unpack();
Expand Down
Loading
Loading