Notional V1 exploit: a 2^128 debt cast to zero
Published Oct 13, 2026
- Project
- Notional V1
- Chain
- Ethereum
- Date
- Sep 3, 2026
- Loss
- $1.73M
- Class
- Integer truncation
- Basis
- Project post-mortem or audit firm analysis
Notional V1 was deprecated in January 2022 but its escrow contract was still live on Ethereum. On September 3–4, 2026 an attacker drained 1,658,524 USDC and 69,257 DAI from it, about $1.73M.
The post-mortem calls it an "integer overflow error related to the mintfCashPair function". Third-party analysis fills in the path: two calls to mintfCashPair() leave an account with an obligation of exactly −2^128. In ExchangeRate._convertToETH the value balance.abs() is cast to uint128 without a range check. 2^128 does not fit in 128 bits, so the cast silently produces zero, and the free-collateral check sees an account with no debt.
The contract was written in Solidity 0.6, where narrowing casts never revert. The attacker swapped the proceeds to about 689 ETH and sent them through Tornado Cash. Notional upgraded the contract and paused all operations.
How to protect your protocol
- Never narrow an integer without a range check. Use a safe-cast helper that reverts on overflow (
SafeCast.toUint128and similar). Solidity 0.8 checks arithmetic, but explicit casts are still unchecked in every version. - Test the exact boundary. Values like 2^128, 2^255 and type maximums are the ones that break casts. A fuzzing run that pushes balances to the edges of each integer type finds this class quickly.
- A deprecated contract that holds funds is still production. Either drain and pause it, or keep it in your audit scope. "Deprecated" in the docs means nothing on-chain.
- Check invariants after state changes, not only inputs. An account whose net position is a huge negative number should never pass a solvency check, whatever the intermediate conversions say.
Sources
- Notional post-mortem (Sept 8, 2026)
- Escrow V1:
0x9abd0b8868546105F6F48298eaDC1D9c82f7f683 - Attack transaction:
0xc3f3e318f7ab2d0daaba59e6ec901d25d1fe8a89aafe2b2b62e3b9aee1a24efa - Pause transaction:
0x012fc554b165b3b3ccf3121018ae26503cab50031fa84adea29253b1cd5831d9 - The cast-to-zero path in
_convertToETHcomes from third-party analysis; the post-mortem namesmintfCashPairand integer overflow.
How this was prepared: compiled from the sources above with the help of automated tools, then reviewed by the Foothold security team before publication.
Give Atlant a task
Could the same thing happen in your code?
Get it checked →