Reproduced Exploit
Forte Float128 — [H-04] eq ignores L_MANTISSA_FLAG / dual encodings
1. Mantissas may be M (38 digits) or L (72 digits); the L flag is bit 241 of the packed word. 2. eq only checks unwrap(a) == unwrap(b). 3. 1.0 encoded as L (1e71 10^-71) and as M (1e37 10^-37) has different bits → eq is false.
Chain
Other
Category
untagged
Date
Apr 2025
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 55706-h-04-unwrapping-while-equating-inside-the-eq-function-fails. Standalone Foundry PoC and full write-up: 55706-h-04-unwrapping-while-equating-inside-the-eq-function-fails_exp in the
evm-hack-registrymirror.
Vulnerability classes: integer-bounds · logic/wrong-condition · data-corruption/accounting-error · misassumption/math-is-safe
Reproduction: self-contained Foundry PoC with only
forge-std— no fork, no RPC. Full trace: output.txt.
Key info#
| Impact | HIGH — eq returns false for two encodings of the same real value |
| Protocol | Forte Float128 solidity library |
| Vulnerable code | Float128.eq — bitwise unwrap comparison |
| Bug class | Equality of representations instead of values |
| Finding | Code4rena 2025-04-forte-float128-solidity-library · #55706 · patitonar |
| Report | code4rena.com/reports/2025-04-forte-float128-solidity-library |
| Source | AuditVault |
| Compiler | ^0.8.24 |
TL;DR#
- Mantissas may be M (38 digits) or L (72 digits); the L flag is bit 241 of the packed word.
eqonly checksunwrap(a) == unwrap(b).1.0encoded as L (1e71 * 10^-71) and as M (1e37 * 10^-37) has different bits →eqis false.
The vulnerable code#
function eq(packedFloat a, packedFloat b) internal pure returns (bool retVal) {
retVal = packedFloat.unwrap(a) == packedFloat.unwrap(b); // @> VULN
}
Fix: normalize (decode + strip trailing zeros / align exponents) before comparing.
Root cause#
Packed floats are a redundant encoding: the same real number has many bit patterns. Bitwise equality is not numerical equality.
Preconditions#
- Two packed floats that represent the same real value with different M/L encodings (common when one path keeps L precision and another normalizes to M).
Attack walkthrough#
packed1= realtoPackedFloat(1e71, -71)→ L encoding of 1.0.packed2= realtoPackedFloat(1, 0)→ M encoding of 1.0.eq(packed1, packed2)returns false.
Diagrams#
Impact#
Any branch that trusts eq for collateral ratios, limit orders, or invariant checks can
misclassify equal values as unequal (or never take the equal path).
Taxonomy (AuditVault)#
[[integer-bounds]]·[[logic/wrong-condition]]·[[data-corruption/accounting-error]]·[[misassumption/math-is-safe]]
Sources#
- AuditVault finding #55706
- Code4rena report
- Reduced from code-423n4/2025-04-forte
@4d6694f/src/Float128.solL1070–L1072
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 55706-h-04-unwrapping-while-equating-inside-the-eq-function-fails_exp (evm-hack-registry mirror).
- AuditVault finding: 55706-h-04-unwrapping-while-equating-inside-the-eq-function-fails.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.