My Projects
Code4renaApril 2025· Rank #21

Forte: Float128 Solidity Library

EVMSolidityLibraryMath

Findings (2)

HighS-126

ln(0) does not revert, violating mathematical invariants and silently returning invalid result

Description

In mathematical terms, the natural logarithm of zero is undefined, tending toward negative infinity. Any reliable numerical system must guard against such input to avoid nonsensical or misleading outputs.

In the Float128 library, calling ln() with a packedFloat value representing zero does not revert or throw an error. Instead, it silently computes and returns an arbitrary result. This behavior contradicts mathematical expectations and can lead to undefined behavior in higher-level protocols that rely on this library.

This issue is especially dangerous because:

  • Zero is a special, well-defined value in the library: its canonical form is a mantissa of all zeroes and exponent -8192.
  • The documentation explicitly emphasizes normalization and special handling of zero.
  • Calling ln(0) should be considered an invalid operation and must revert, similar to 1 / 0.

Impact

  • Mathematical inconsistency: protocols relying on ln() for calculations involving small or zero values will behave incorrectly.
  • Silent errors: dependent protocols (e.g., AMMs, lending rates, interest curves) may silently compute wrong values.
  • Downstream propagation: operations that trust the result may propagate invalid values or precision errors.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "src/Float128.sol";
contract LnZeroPoC is Test {
using Float128 for int256;
using Float128 for packedFloat;
function test_lnZeroShouldRevert() public {
packedFloat zero = int256(0).toPackedFloat(-8192); // Canonical zero
// Expect revert when computing ln(0)
vm.expectRevert();
zero.ln();
}
}

Terminal output:

Ran 1 test for test/LnZeroPoC.t.sol:LnZeroPoC
[FAIL: next call did not revert as expected] test_lnZeroShouldRevert() (gas: 63682)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.20ms

Recommended Fix

In the implementation of Float128.ln(packedFloat x), add an explicit check at the beginning:

require(x != ZERO, "ln undefined for zero");

Alternatively, decode the mantissa and revert if it is zero:

(int256 mantissa, ) = decode(x);
require(mantissa != 0, "ln undefined for zero");

This prevents accidental or malicious misuse and aligns with both mathematical standards and developer expectations.

Affected Code

Ln.sol#L56-L77

HighS-31

Float128.eq() breaks logical equality for numerically equal values with different encodings

Description

The Float128.eq() function attempts to determine equality between two packedFloat values by directly comparing their underlying bitwise encoding:

function eq(packedFloat a, packedFloat b) internal pure returns (bool) {
return packedFloat.unwrap(a) == packedFloat.unwrap(b);
}

However, this logic fails to account for the fact that the same mathematical value can be represented by multiple valid packedFloat encodings.

For example:

  • 123.45 can be encoded as 12345e-2, 123450e-3, or 1234500e-4
  • All decode to the same numeric value: 1234500 × 10⁻⁴ = 123.45
  • Yet their packed representations differ, and Float128.eq() will return false

This creates a logical inconsistency: two values that are mathematically equal fail equality checks.

Impact

This can lead to serious issues in downstream protocols:

  • Comparison-based logic: Contracts relying on equality will behave incorrectly
  • Set-like behavior: You cannot de-duplicate equivalent values
  • Mappings or lookups: Equivalent values will hash differently and map incorrectly
  • Math-sensitive invariants: Fail due to mismatches in logical float values

This undermines the correctness and trustworthiness of a core math utility and can silently break downstream protocols.

Proof of Concept

contract FloatEqualityPoC is Test {
using Float128 for int256;
using Float128 for packedFloat;
function test_eqFailsForSameValueDifferentEncoding() public {
packedFloat a = Float128.toPackedFloat(123450, -3); // 123.45
packedFloat b = Float128.toPackedFloat(1234500, -4); // 123.45
(int am, int ae) = Float128.decode(a);
(int bm, int be) = Float128.decode(b);
// Assert mathematical equality
int vA = am * int(10 ** uint(-ae));
int vB = bm * int(10 ** uint(-be));
assertEq(vA, vB, "Decoded values must be equal");
// eq() fails due to different bit encoding
assertFalse(Float128.eq(a, b), "Float128.eq fails on equivalent values");
}
}

Terminal output:

Ran 1 test for test/FloatEqualityPoC.t.sol:FloatEqualityPoC
[FAIL: Float128.eq fails on equivalent values] test_eqFailsForSameValueDifferentEncoding() (gas: 6946)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped

Recommended Fix

Rewrite Float128.eq() to compare decoded values in normalized form.

Option 1 — Compare decoded values directly:

function eq(packedFloat a, packedFloat b) internal pure returns (bool) {
(int am, int ae) = Float128.decode(a);
(int bm, int be) = Float128.decode(b);
return am == bm && ae == be;
}

Option 2 (safer) — Normalize and compare:

function eq(packedFloat a, packedFloat b) internal pure returns (bool) {
return Float128.normalize(a) == Float128.normalize(b);
}

Affected Code