OAK — OnChain Attack Knowledge

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

Loss
1,300 ETH (~$1.4M). The drained balance consisted of the protocol's own internal testing funds — no user funds were affected, a distribution that makes this one of the few cases in the corpus where the party that shipped the defect absorbed the entire consequence. Proceeds were laundered through Tornado Cash.
OAK Techniques observed
OAK-T12.006 (NFT-Collateral Lending Manipulation — primary, and the canonical transfer-callback-reentrancy anchor for the Technique. Omni was an NFT money market: deposit an ERC-721, borrow wETH against it. Returning custody of an NFT uses safeTransferFrom, which by design of the ERC-721 standard invokes onERC721Received on the recipient — so the protocol handed execution to attacker-controlled code in the middle of a state transition. See techniques/T12.006-nft-collateral-lending-manipulation.md). OAK-T9.005 (Reentrancy — the confirmed mechanism, in an unusual double form: the vulnerability was exploited across two different functions of the same contract, neither of which carried a reentrancy lock and both of which violated checks-effects-interactions around the NFT transfer. The attacker deposited Doodles NFTs, borrowed wETH, used an NFT acquired with the loan as collateral to borrow again, and Omni failed to register that as a new position — permitting withdrawal of the NFTs without repayment. See techniques/T9.005-reentrancy.md). OAK-T7.001 (Mixer-Routed Hop — proceeds moved through Tornado Cash).
Attribution
pseudonymous. No named individual or group, no published identity, and no link to a tracked OAK actor. The technical account was published by Immunefi, with independent analyses from other researchers; nothing in the public record identifies the operator.
Key teaching point
safeTransferFrom is an external call to untrusted code, and the word "safe" in its name is about a different hazard entirely. ERC-721's safeTransferFrom exists to prevent tokens being sent to contracts that cannot handle them — it invokes onERC721Received to ask the recipient to acknowledge. That acknowledgement is arbitrary attacker code executing inside your transaction. In an NFT lending protocol, the moment collateral is returned is precisely the moment the loan record is being updated, which makes it precisely the wrong moment to yield control. Every developer who has internalised "ERC-777 and ETH transfers reenter" needs the same reflex for ERC-721 and ERC-1155 transfer hooks, and the evidence that the reflex does not exist is that both vulnerable functions here shipped without guards. The generalisable rule is mechanical and old: complete every state change before any external call, and put a reentrancy guard on anything that touches a token transfer. What makes this case worth a corpus slot rather than a footnote is the double form — the exploit chained re-entries across two separate functions, so auditing each function in isolation for "does this reenter itself?" would have passed both. The question to ask is not whether a function reenters itself but which set of functions share the state that the callback can observe half-updated.

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-777 tokensReceived all 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

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.

Techniques demonstrated (3)