Clawditor
← all research
post-mortemhigh$1.7M lost

Notional Finance V1 $1.73M Integer Overflow: uint128 Truncation Erases Debt in Solidity 0.6.x

Clawditor Research·Published Oct 19, 2026·Incident Sep 3, 2026
Notional Finance V1

An attacker exploited an unchecked uint128 narrowing cast in Notional Finance's legacy V1 escrow to craft two fCash positions whose combined debt equalled exactly 2¹²⁸ — silently truncating to zero and bypassing the free-collateral check, draining $1.73M in USDC and DAI.

Root Cause

Notional Finance V1's free-collateral calculation performed an unchecked narrowing cast from a signed integer to uint128 in Solidity 0.6.x — a compiler version that does not provide automatic overflow protection. If an account's absolute debt equalled exactly 2¹²⁸, the cast silently truncated to zero, making the protocol treat a massively under-collateralised account as fully collateralised.

// Solidity ^0.6.x — no SafeCast, no checked arithmetic
function getFreeCollateral(address account) internal view returns (int256) {
    int256 netDebt = _getNetDebt(account);   // may be large negative value

    // VULNERABILITY: abs(-2^128) == 2^128; uint128(2^128) overflows to 0
    uint128 debtMagnitude = uint128(netDebt.abs());

    int256 collateralValue = _getCollateral(account);
    // With debtMagnitude == 0, this always returns collateralValue >= 0
    return collateralValue - int256(debtMagnitude);
}

// Collateral check passes even with 2^128 real debt outstanding
require(getFreeCollateral(account) >= 0, "Undercollateralized");

Attack Steps

#Step
1Attacker opens fCash debt position of size 1 in payer account
2Attacker opens second fCash debt of size uint128.max in the same payer account
3Combined absolute debt = 2¹²⁸; uint128(2^128) truncates to 0
4Free-collateral check passes (collateral − 0 ≥ 0) despite massive real debt
5Attacker transfers the unbacked collateral position to a second account
6Second account borrows against the phantom collateral, draining the escrow
7~$1.73 M (≈ 1.66 M USDC + 69 K DAI) swapped to ~689 ETH and laundered via Tornado Cash

Impact

  • ~$1.73 M drained from the legacy V1 Escrow on Ethereum (Sept 3–4, 2026)
  • Protocol team paused the V1 contract immediately after detection, protecting ~$60 K remaining
  • V2 was not affected; the bug exists only in the legacy V1 codebase
  • SlowMist analysis dubbed the attack "The Vanishing Debt"

Lessons for Auditors

  1. Use SafeCast or Solidity ≥0.8 for all type-narrowing casts. uint128(x) in 0.6.x is a silent truncation; replace with SafeCast.toUint128(x) which reverts on overflow.
  2. Fuzz collateral calculations with boundary values. Inputs of uint128.max, int128.min, and multiples of 2¹²⁸ would surface this class of bug in minutes.
  3. Audit dormant legacy contracts that still hold live funds. V1 was superseded but never drained; $1.7 M in unreviewed legacy code is a standing invitation to attackers.
  4. Verify arithmetic invariants across coordinated positions. A property test asserting that the sum of all fCash pair debts equals net zero for any combination of positions would have caught the overflow path.
attack patterns
precision-mathdefi-lendingerc20notional-financeethereuminteger-overflow
sources