Worked example · 2022-07
Omni Protocol — handing an NFT back with safeTransferFrom calls the borrower's code mid-transition, and two unguarded functions turned that callback into a double reentrancy — Omni Protocol (Ethereum) — 2022-07-10
Summary
Omni Protocol was an NFT money market on Ethereum: users deposited ERC-721 tokens as collateral and borrowed wrapped ETH against them.
Two functions of the same contract were vulnerable. Neither had a reentrancy lock, and both performed the ERC-721 safeTransferFrom — whose onERC721Received callback hands execution to the recipient — before committing the state changes that the transfer implied. That ordering is a checks-effects-interactions violation, and it made the receiving contract's callback an execution slot inside an incomplete transition.
On 2022-07-10 an attacker chained both. Using Doodles NFTs, they deposited collateral and borrowed wETH, then used an NFT obtained through the initial loan as collateral to borrow again. Because the protocol did not recognise the second borrow as a new position, the attacker was able to withdraw the NFTs without repaying the loans they secured. The composed result — a double reentrancy across the two functions — extracted 1,300 ETH (~$1.4M).
The drained funds were the protocol's own internal testing capital; no user funds were lost. The proceeds were routed through Tornado Cash. Immunefi published the technical hack analysis.
Timeline (UTC)
| When | Event | OAK ref |
|---|---|---|
| (standing) | Two functions of the same Omni contract lack reentrancy locks and call ERC-721 safeTransferFrom before committing dependent state |
(standing T9.005 / T12.006 surface) |
| 2022-07-10 | Attacker deposits Doodles NFTs and borrows wETH | (setup) |
| 2022-07-10 | Attacker uses an NFT acquired via the initial loan as collateral for a further borrow; the protocol does not register it as a new position | T12.006 |
| 2022-07-10 | onERC721Received callbacks are chained across both unguarded functions — a double reentrancy — allowing NFT withdrawal without repayment |
T9.005 |
| 2022-07-10 | 1,300 ETH (~$1.4M) extracted; the balance consists of the protocol's internal testing funds, so no user loses assets | T5.001 outcome |
| 2022-07-10 onward | Proceeds laundered through Tornado Cash | T7.001 |
| post-event | Immunefi publishes the hack analysis identifying both vulnerable functions and the double-reentrancy composition | (analysis) |
What defenders observed
- Pre-event (treat every token-transfer hook as an external call).
safeTransferFrom(ERC-721),safeTransferFrom/safeBatchTransferFrom(ERC-1155), and ERC-777tokensReceivedall execute recipient code. Any state a function relies on must be committed before those calls, not after (M10). - Pre-event (audit reentrancy across function sets, not per function). Neither function here reenters itself in an obvious way; the exploit crossed between them over shared state. Reentrancy review should enumerate the state each externally-calling function leaves half-written and which other entrypoints read it.
- Pre-event (guards are cheap and their absence is the finding). A standard reentrancy guard on both paths closes the incident outright. That both shipped without one, in a protocol whose core operation is transferring ERC-721s, is the reusable observation about how the NFT-lending cohort was built in 2022 (M02, M16).
- At-event (position accounting must be idempotent under re-entry). The protocol failing to register the second borrow as a new position is the accounting half of the bug. Position creation and collateral escrow should be one committed transition, checkable by an invariant rather than by control flow.
- Response (the loss landed entirely on the protocol's own capital). Only internal testing funds were exposed — an accident of what happened to be in the contract, not a control. It is worth recording as the outcome rather than as a mitigation: the same defect against a live user base would have produced a proportionally larger and differently distributed loss.
Public references
[immunefiomni2022]— Immunefi, "Hack Analysis: Omni Protocol, July 2022" (the primary technical account: reentrancy exploited across two functions of the same contract, both lacking reentrancy locks and both violating checks-effects-interactions around ERC-721safeTransferFrom, composed into a double-reentrancy attack): https://medium.com/immunefi/hack-analysis-omni-protocol-july-2022-2d35091a0109[theblockomni2022]— The Block, "Hacker drains $1.4 million worth of ETH from NFT lender Omni" (the $1.4M figure, the 2022-07-10 dating, and the NFT-lending profile): https://www.theblock.co/post/156800/hacker-drains-1-4-million-worth-of-eth-from-nft-lender-omni[beincryptoomni2022]— BeInCrypto, "NFT Protocol OMNI Suffers Reentrancy Attack, Loses 1,300 ETH in Testing Funds" (the 1,300 ETH denomination and the finding that only internal testing funds were affected, with no user funds lost): https://beincrypto.com/nft-protocol-omni-reentrancy-attack-loses-1300-eth-testing-funds/[harboromni2022]— Harbor (Coinmonks), "Analysis of the hack of the NFT financial protocol: Reentrancy attack and flash loan" (independent walk-through of the Doodles-collateral borrow sequence and the withdrawal-without-repayment outcome): https://medium.com/coinmonks/omni-protocol-hack-analysis-july-2022-1aea23166a75[dailycoinomni2022]— DailyCoin, "1,300 ETH Stolen from NFT Lending Platform OMNI in Re-entrancy Exploit" (corroboration of the amount and the Tornado Cash laundering): https://dailycoin.com/1300-eth-stolen-from-nft-lending-platform-omni-in-re-entrancy-exploit/
Discussion
Omni is filed under T12.006 rather than only under T9.005 because the reentrancy vector was not incidental to the product — it was the product. An NFT money market's core operation is taking custody of an ERC-721 and later giving it back, and the standard's own "safe" transfer is a callback. A fungible money market can be written without ever making an external call into a counterparty; an NFT money market cannot. That is a structural property of non-fungible collateral, and it is why the reentrancy exposure in this sector is systematically higher than the generic reentrancy technique page would suggest.
The double form is the part contributors should carry forward. Reentrancy review has largely converged on a per-function question — does this function's external call let someone re-enter it before it finishes? Omni's exploit crossed two functions, using state that one left inconsistent and the other read. The productive framing is a state-based one: for each externally-calling function, list the storage it mutates after the call, then list every other entrypoint that reads that storage. Anything in the intersection is a candidate pair. Guards on both paths make the question moot, which is why the guard, not the analysis, is the recommendation.
The loss distribution is the last thing worth preserving. Only internal testing funds were taken, so the protocol paid for its own bug in full — an outcome that is almost never available and that here was luck, not design. OAK records it plainly because the counterfactual is what makes the case instructive: the identical defect on a populated market redistributes the same mechanism onto users who had no way to evaluate whether the contract they deposited a Doodle into had a reentrancy guard. Two weeks earlier, XCarnival demonstrated the other way this sector loses custody-belief — a stale order record rather than a mid-transition callback — and the pair is the reason T12.006 exists as a Technique instead of as two unrelated bugs.