In early September, Notional Finance’s legacy V1 contract stack (ERC-1155 fCash + Portfolios + Escrow) was drained by an attacker using exactly two public transactions — no admin keys, no privileged calls, nothing exotic. The loss was roughly $1.727M in DAI and USDC. The attacker never touched a price oracle and never needed a flash loan. What they needed was something more mundane and, frankly, more dangerous: a transfer path that skipped a balance check, and an integer ladder that quietly wrapped around.
At PandaSecurity, we walk through a couple of real on-chain incidents every week — not for the drama, but because the patterns inside them keep showing up, almost verbatim, in the codebases we’re asked to review. The Notional V1 incident is a clean example of that. The bug itself isn’t conceptually hard. What’s hard to explain is how long it sat there unnoticed.
The timing was tight and deliberate. The first transaction set up the malformed position; the second, roughly 73 seconds later, settled and withdrew. That gap wasn’t accidental — it was timed against the exact fCash maturity window.
To understand the bug, you need to understand how Notional V1 treats an fCash transfer.
In most protocols, an ERC-1155 transfer just moves a token the sender already owns. Notional V1 does something different: every safeTransferFrom call is treated as minting a matched pair of positions, not moving an existing one.
from account is booked a CASH_PAYER leg (assetType 2 — a debt, i.e. a short)to account is booked a CASH_RECEIVER leg (assetType 3 — a claim, i.e. a long)Here’s the part that matters: the protocol mints this pair regardless of whether from ever held the underlying position. Call safeTransferFrom, and the system assumes you’re “selling” something — and dutifully records both the debt and the claim, even if you never had the asset to begin with.
If the minted quantity were small, this would just be a bookkeeping quirk with minor implications for liquidation logic. But the attacker minted a short sized at type(uint128).max — roughly 2^128. Notional’s Escrow tracks each account’s net position in a uint128 cash ladder. When the netting arithmetic hits a value near 2^128, the ladder overflows. An account that should read as catastrophically under-collateralized instead reads as non-negative, and sails straight through the protocol’s own solvency check (freeCollateralViewAggregateOnly >= 0).
The counterparty’s receivable — which should never have existed in the first place — now looks, from the protocol’s own bookkeeping, like a fully legitimate claim that will settle normally at maturity. Once maturity hits, calling settleMaturedAssets followed by Escrow.withdraw does exactly what it’s supposed to do with a “real” claim: it hands over actual DAI and USDC sitting in the Escrow, sized to the face value of that claim.
Root cause, in one line: the mint size was pulled straight from the victim contract’s own live balance, and the ledger meant to absorb it was only 128 bits wide.
The attacker used four helper contracts — not EOAs — specifically to keep each account’s fCash ladder isolated from the others. That separation was intentional; it’s what let the overflow land cleanly on one account without disturbing the others.
| Role | Purpose |
|---|---|
| A1 | Receives a 1-unit hedge position, used to confirm the exploit contract’s own collateral ratio nets to zero |
| B_HUB | Receives the 2^128-1 seed short — the account where the overflow actually triggers |
| C_DAI | Receives a long position from B_HUB sized to the Escrow’s live DAI balance |
| D_USDC | Receives a long position from B_HUB sized to the Escrow’s live USDC balance |
Transaction 1 (setup, timestamp 1,788,479,927):
setApprovalForAll between them.Transaction 2 (drain, timestamp 1,788,480,095 — about 95 seconds after maturity):
settleMaturedAssets to settle their now-matured claims.Escrow.withdraw to pull the real DAI and USDC.Two transactions. No signer permissions, no admin path, no external dependency — just a sequence of ordinary calls to the protocol’s own public functions.
A few things about this incident are worth writing down for anyone doing contract design or audit work:
“Transfer that secretly mints” is a semantic that reviewers tend to trust by default. Most audits, seeing safeTransferFrom, immediately check for reentrancy and access control. Fewer people stop to ask: “does this transfer actually require from to hold the token first?” When a protocol’s transfer function has non-standard mint-on-transfer behavior baked in, a standard ERC-1155 security checklist simply won’t catch it — because the checklist assumes the standard semantics apply.
Sizing the attack off the victim’s own live balance is hard to catch with static rules. The attacker didn’t hardcode any numbers. setup() reads IERC20(DAI).balanceOf(address(ESCROW)) directly and uses that as the mint size. That means the exploit scales automatically to whatever the Escrow happens to be holding — there’s no fixed threshold a simple limit check would flag.
Fixed-width integer ladders without overflow protection are a classic landmine in older code. Notional V1 predates some of the conventions that are now standard practice — Solidity 0.8’s built-in overflow checks weren’t necessarily applied consistently across every accounting type back then. Any place in a codebase today that still uses a fixed-width type (especially something in the “looks big enough” range like uint128) to track net positions, balances, or collateral ratios deserves a dedicated second look — particularly when that value derives from user-controlled input or the protocol’s own live state.
If your protocol has any “transfer that implicitly mints a position” pattern — common in perpetuals, options, fixed-rate lending, or share-issuance logic for structured products — here’s where we’d start:
from already holds the corresponding position, or require explicit posted collateral. Never let the default path silently mint an unbacked pair.int256 or a properly checked wider type. Anywhere a value gets narrowed (e.g. uint128(abs())), that truncation needs to be explicit and reverting, not silent.settleMaturedAssets generally assumes that anything reaching the settlement stage was minted legitimately. If the minting step has a flaw, settlement will faithfully execute on top of it. The trust assumptions baked into settlement deserve their own line item in an audit scope, not just a review of the minting function.The Notional V1 incident is another data point for something we keep coming back to in our audits: large losses often don’t come from a deep cryptographic flaw — they come from a business-logic assumption that was never lined up against the actual bounds of a data type. Treating fCash transfers as implicit mints may well have been a reasonable simplification when the system was first designed. But it quietly pushed an ownership check that belongs at the transfer layer down into the ledger layer’s overflow behavior — and that ledger layer was 128 bits wide.
For teams still running legacy contract stacks, this is a good reminder that old code doesn’t become safe just because it’s been running quietly for years — especially when it involves custom transfer semantics and fixed-width accounting. PandaSecurity treats “implicit mint plus fixed-width ledger” as a high-priority pattern in every review, precisely so it gets caught on our side of the table instead of an attacker’s.
This attack-chain breakdown is based on the public PoC and on-chain transaction data, reproducible line-by-line in the evm-hack-analyzer environment. If your protocol has custom transfer semantics, share-minting logic, or fixed-width integer ledgers, reach out to the PandaSecurity Team for a dedicated review.
UNCX's token vesting contract lost roughly $87.7K when an attacker exploited a share-accounting flaw in release(), topUpVesting(), and cancel(). We break down the root cause, the exact attack sequence, and what builders should take away from it — without the jargon.
Autheo’s THEO token lost 92% after attackers used a Hyperlane TrustedRelayerIsm misconfiguration to mint 19 million unbacked tokens on Base and extract roughly $1.76 million. Here’s how the attack worked and why single-key security models remain dangerous for cross-chain assets.
Uniswap v4 hooks explained: the PoolManager singleton, flash accounting, and what the Cork and Bunni exploits teach us about hook security. A PandaSecurity deep dive.
An attacker forced a Uniswap V4 vault into a zero-liquidity state, redeemed one minimal share unit, and withdrew roughly 14,838.47 DAI. Panda Security breaks down the transaction, vulnerable code, root cause, and practical fixes.