Reproduced Exploit
Forte Float128 `Ln.ln` silently accepts non-positive inputs (mispriced settlement / stolen collateral)
Chain
Other
Category
math
Date
Jan 1970
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: 55705-h-03-natural-logarithm-function-silently-accepts-invalid-non. Standalone Foundry PoC and full write-up: 55705-h-03-natural-logarithm-function-silently-accepts-invalid-non_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/math/missing-input-validation · vuln/defi/mispricing
Reproduction: the test deploys the REAL (byte-identical, audited-commit)
Float128+Lnlibrary and a minimal REAL consumer (SpreadOptionVault) that prices a cash-settled log-contract option asnotional * ln(settlePrice - strike). It shows an out-of-the-money option (a worthless position that must pay0) being paid693.147180559945309417collateral tokens — drained one-for-one from an honest writer — purely becauseLn.lnreturnsln(|-2|) = ln(2)instead of reverting on the invalid (negative) log input.
Root cause#
The natural-log entry point Ln.ln extracts the mantissa with a mask that omits the sign bit and never checks the value's sign or zero-ness:
function ln(packedFloat input) public pure returns (packedFloat result) {
uint mantissa;
int exponent;
bool inputL;
assembly {
inputL := gt(and(input, MANTISSA_L_FLAG_MASK), 0)
mantissa := and(input, MANTISSA_MASK) // L69 - root cause: MANTISSA_SIGN_MASK (bit 240) is ignored
exponent := sub(shr(EXPONENT_BIT, and(input, EXPONENT_MASK)), ZERO_OFFSET)
}
// ... (special-case for the smallest representable number) ...
result = ln_helper(mantissa, exponent, inputL); // proceeds unconditionally, even for input <= 0
}
MANTISSA_MASK covers bits 0..239; the sign lives in MANTISSA_SIGN_MASK (bit 240), which ln never reads. So for a negative input the function computes ln(|input|) (e.g. ln(-2) returns exactly ln(2) = 0.69314718055994530941723212145817656807), and for zero it returns a finite garbage value (-18781.450104..., when the true value is -∞). The log domain is x > 0; a mathematical library is expected to fail explicitly on an invalid input, but this one silently returns a plausible, wrong, real number.
The recommended fix (accepted by the Forte team) reverts when input == 0 or when MANTISSA_SIGN_MASK is set.
Why it is exploitable (the consumer path)#
Any contract that trusts ln to police its own domain will silently misprice. SpreadOptionVault is a minimal, faithful example: it settles a "log-contract" option as notional * ln(settlePrice - strike), which is meaningful only when the option is in the money (settlePrice > strike, so the log argument is positive). Trusting the library, the vault does not re-check delta > 0:
packedFloat delta = settlePrice.sub(p.strike); // NEGATIVE when out of the money
packedFloat payoffPF = p.notional.mul(Ln.ln(delta)); // ln SHOULD revert here for delta <= 0
Because Ln.ln(delta) does not revert for delta <= 0, an out-of-the-money holder (whose option is worthless and should pay 0) is paid the same amount as an in-the-money holder — draining the writers' collateral.
Exploit walkthrough (real numbers)#
- A writer backs the vault with 10,000 collateral tokens (a real ERC20).
- Control (in the money): a holder with
notional = 1000,strike = 100settles atsettlePrice = 102.delta = +2, payoff= 1000 * ln(2) = 693.147180559945309417tokens — a legitimate payout. - Theft (out of the money): the attacker holds the same
notional = 1000,strike = 100and settles atsettlePrice = 98. The option is worthless and must pay0. Butdelta = 98 - 100 = -2(a negativeFloat128), andLn.ln(-2)returnsln(2)— identical to the in-the-money case. The vault pays the attacker 693.147180559945309417 tokens. - Harm: the attacker's ERC20 balance rises by
693.147180559945309417and the vault's collateral falls by exactly the same amount — a real theft from the honest writer on a position that should have paid nothing.
The test also asserts the mechanism directly: ln(-2) mantissa/exponent equal ln(+2) (proving the sign bit was discarded), and ln(0) returns the finite value -18781450104493291890957123580748043517e-33 from the finding.
Reproduction#
_shared/run-poc/run_poc.sh 55705-h-03-natural-logarithm-function-silently-accepts-invalid-non_exp -vvvvv
Expected result: 3 passed. See test/55705-h-03-natural-logarithm-function-silently-accepts-invalid-non_exp.sol. The real audited sources — src/Ln.sol, src/Float128.sol, src/Types.sol, lib/Uint512.sol — are vendored byte-identical to the audited commit. Ln is a public-function library, so it is linked at a fixed address (0x…6c6e) via foundry.toml and vm.etched in the test. The only non-audited code is the minimal consumer under src/consumer/ (MiniERC20, SpreadOptionVault) that demonstrates the harm — the vulnerable ln on the exploit path is the real library.
Sources#
- AuditVault finding #55705
- Forte
Ln.sol@4d6694f6 - Code4rena 2025-04 Forte (Float128 Solidity library) report
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 55705-h-03-natural-logarithm-function-silently-accepts-invalid-non_exp (evm-hack-registry mirror).
- AuditVault finding: 55705-h-03-natural-logarithm-function-silently-accepts-invalid-non.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Forte Float128
Ln.lnsilently accepts non-positive inputs (mispriced settlement / stolen collateral)". - Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
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.