Clawditor
← all research
post-mortemhigh$1.7M lost

Notional Finance $1.73M Exploit: Integer Overflow Turns -2^128 Debt into Zero Collateral

Clawditor Research·Published Sep 5, 2026·Incident Sep 4, 2026
Notional Finance

An attacker called `mintfCashPair()` twice to manufacture a -2^128 liability that an unsafe `uint128()` downcast silently truncated to zero, making a massive fabricated debt appear fully collateralised and allowing a $1.73M drain.

Root Cause

Notional Finance's free-collateral check contained an unsafe signed-to-unsigned downcast. When computing net collateral, the protocol cast an int256 liability value to uint128. For an attacker-controlled value of -2^128, this downcast silently produces 0, causing the health check to report the position as perfectly solvent despite an enormous fabricated debt.

// Vulnerable pattern (simplified) in free-collateral calculation:
function _getNetCollateral(address account) internal view returns (int256) {
    int256 netCashBalance = _sumPositionsAndLiabilities(account);
    // BUG: int256(-2^128) cast to uint128 wraps to 0
    // A massive negative liability reads as zero debt
    return int256(uint128(netCashBalance));
}

// Safe alternative using OpenZeppelin SafeCast:
// import "@openzeppelin/contracts/utils/math/SafeCast.sol";
// int128 safeValue = SafeCast.toInt128(netCashBalance); // reverts on overflow

The attacker triggered this by calling mintfCashPair() twice with crafted arguments that caused the internal liability accumulator to underflow to exactly -2^128.

Attack Steps

StepActionDetail
1Fund attacker EOAFunded from 0xC954...De69
2First mintfCashPair() callCreates initial paired fCash asset + liability
3Second mintfCashPair() callCrafted amounts cause int256 liability to underflow to -2^128
4Collateral check bypasseduint128(-2^128) == 0; position reports as zero net debt
5Borrow/drain DAI69,242 DAI drained from escrow contract
6Drain USDC1,658,423 USDC drained from escrow contract
7Swap to ETHDAI + USDC swapped for 689.2 ETH
8ObfuscateProceeds routed through Tornado Cash

Setup tx: 11:58 PM UTC September 3, 2026
Drain confirmed: 12:01 AM UTC September 4, 2026
Attacker address: 0xDaCC...Ce38

Impact

  • Total loss: ~$1,730,000
  • Assets: 69,242 DAI + 1,658,423 USDC → 689.2 ETH
  • Chain: Ethereum mainnet
  • Protocol: Notional Finance escrow contract
  • Date: September 3–4, 2026

Lessons for Auditors

  1. Never downcast signed integers to unsigned types in financial arithmetic. int256 → uint128 or similar conversions silently discard sign information for large negative values, turning debt into apparent collateral.
  2. Use SafeCast (OpenZeppelin) or equivalent everywhere. SafeCast.toInt128(x) reverts on overflow; the raw cast does not.
  3. Fuzz collateral calculations with extreme boundary values. A fuzz suite targeting mintfCashPair() with inputs near ±2^127 and ±2^128 would surface this class of bug before deployment.
  4. The mintfCashPair() function should enforce that aggregate liability positions remain within safe int128/uint128 ranges before updating state.
  5. Invariant testing: assert that the sum of all fCash liabilities across the protocol is always >= 0 after any state-changing call. A negative aggregate is a red flag for overflow exploitation.
attack patterns
precision-mathdefi-lendingerc20
sources