OAK — OnChain Attack Knowledge

Worked example · 2022-06

XCarnival — the borrow path checked that a pledge order existed, never that the Ape was still in the vault, so one BAYC backed loan after loan — XCarnival (Ethereum) — 2022-06-26/27

Loss
3,087 ETH (~$3.8M). The attacker's operating wallet was pre-funded with 120 ETH withdrawn from Tornado Cash. Roughly 50% was recovered: 1,467 ETH returned on 2022-06-27 at 13:45 UTC+8, against a 1,500 ETH bounty XCarnival offered publicly to the returning address, together with an explicit waiver of legal action. Net realised loss to the protocol was therefore on the order of $1.9M after the return and before the bounty.
OAK Techniques observed
OAK-T12.006 (NFT-Collateral Lending Manipulation — primary, confirmed mechanism, and the canonical collateral-state-desynchronisation anchor for the Technique. XCarnival's borrow path validated that a pledge order was open; it never re-checked whether the NFT that order referenced was still held by the protocol. Withdrawing the collateral did not invalidate the order. The attacker pledged Bored Ape Yacht Club #5110, withdrew it, and borrowed against the still-open order — repeatedly, through a set of self-deployed contracts, each pledging the same Ape. See techniques/T12.006-nft-collateral-lending-manipulation.md). OAK-T5.001 (Hard Drain — the on-chain manifestation: the ETH pool emptied against collateral that was not there). OAK-T7.001 (Mixer-Routed Hop — the 120 ETH of working capital came out of Tornado Cash before the first pledge, which is a premeditation signal rather than an opportunistic one).
Attribution
pseudonymous. The returning address, 0xb7CBB4d43F1e08327A90B32A8417688C9D0B800a, was named publicly by XCarnival when it offered the 1,500 ETH bounty and waived legal action. XCarnival additionally stated that security agencies had "tentatively determined the hacker's geographic location" — OAK records this as the protocol's claim, not as an established fact; no identification was ever published, no enforcement action followed, and nothing links the operator to a tracked OAK actor.
Key teaching point
A loan is a claim on a specific thing, and the protocol has to be able to prove it still holds that thing at the moment it lends — not at the moment the borrower promised it. XCarnival's check was in the right function and asked the wrong question: is there an open order? rather than does the vault still own token #5110? Those two questions have identical answers right up until the instant that matters. The fix is a single comparison — ownerOf(tokenId) == vault evaluated at borrow time — and its absence converted one Bored Ape into unbounded collateral. The structural lesson generalises past NFTs: whenever a protocol's authorisation state and its custody state live in separate storage, some transition will desynchronise them, and the safe design is to make custody the source of truth rather than a thing the records claim. For non-fungible collateral this is unusually cheap, because ownership is a single on-chain read against a canonical contract — there is no balance to aggregate and no oracle involved. The second lesson is about what indivisibility does to blast radius. In a fungible market, borrowing against collateral you have withdrawn drains at most the value you notionally deposited; here the same Ape was re-pledged through fresh contracts as many times as the attacker cared to deploy, so the ceiling was not the collateral's value but the pool's.

Summary

XCarnival was an NFT-collateralised lending protocol on Ethereum: a borrower pledged an NFT into the protocol, an order was recorded against that pledge, and ETH was lent against it.

The borrow function did not verify that the pledged NFT remained in the protocol's custody. Withdrawal of the collateral left the corresponding orderId open and borrowable. On 2022-06-26/27, an attacker exploited that gap directly: pledge BAYC #5110, withdraw the Ape, borrow against the still-open order, and repeat — creating multiple pledge orders from multiple attacker-deployed contract addresses, all resolving to the same single NFT.

The working capital for the attack — 120 ETH — was withdrawn from Tornado Cash beforehand. The extraction totalled 3,087 ETH (~$3.8M). XCarnival suspended the affected contract once the drain was identified.

Recovery was unusually fast and unusually public. At 13:45 UTC+8 on 2022-06-27 the attacker returned 1,467 ETH. XCarnival publicly offered the returning address a 1,500 ETH bounty and stated it would not pursue legal action; the attacker accepted. The protocol also stated that security agencies had tentatively located the operator — a claim never substantiated publicly, and plausibly part of the negotiating posture that produced the return.

Timeline (UTC)

When Event OAK ref
(standing) XCarnival's borrow path validates that a pledge order is open, without re-checking that the referenced NFT is still held by the protocol; withdrawal does not invalidate the order (standing T12.006 surface)
(pre-event) Attacker withdraws 120 ETH from Tornado Cash as working capital T7.001
2022-06-26/27 Attacker pledges BAYC #5110, withdraws it, and borrows against the still-open order; repeats across multiple self-deployed contracts, all referencing the same Ape T12.006
2022-06-26/27 3,087 ETH (~$3.8M) extracted from the lending pool T5.001
2022-06-27 XCarnival suspends the affected contract M34
2022-06-27 13:45 (UTC+8) Attacker returns 1,467 ETH (negotiated return)
2022-06-27 onward XCarnival names the returning address publicly, offers a 1,500 ETH bounty, waives legal action; claims security agencies have tentatively determined the operator's geographic location M32, (unverified claim)
post-event SlowMist publishes the vulnerability analysis; no identification, no enforcement action (analysis)

What defenders observed

  • Pre-event (custody must be re-read at use time, not trusted from record time). The entire loss is the distance between "an order says an Ape is pledged" and "the vault owns that Ape right now". ownerOf(tokenId) == vault at borrow time is a one-line precondition and closes the class (M16).
  • Pre-event (state transitions on custody and on the loan record must be atomic). Collateral withdrawal should close every order referencing that token in the same transaction. Anywhere those two updates can be separated, an attacker will separate them.
  • Pre-event (a per-collection borrow-capacity invariant bounds the unknown bug). Σ debt against BAYC ≤ escrowed BAYC × per-token limit would have capped the loss at one Ape's worth regardless of the state defect (M11).
  • At-event (the same tokenId across many pledge orders is a decisive signal). Multiple open orders referencing one token, from freshly deployed contracts, has no legitimate population in an NFT lending market. It is trivially queryable and would have fired on the second iteration.
  • At-event (mixer-funded operating wallets precede this class). 120 ETH out of Tornado Cash before the first pledge is a pre-positioning signal; funder-graph screening on new large borrowers is cheap (M04).
  • Response (a pause plus a public bounty recovered half). XCarnival's combination — suspend, publicly name the address, offer 1,500 ETH, waive legal action — produced a 1,467 ETH return within roughly a day. Whatever one thinks of negotiating with an attacker, the outcome is data: NFT-lending exploits have an unusually good return record, and the traceability of NFT collateral is part of why (M32, M35).

Public references

Discussion

XCarnival is the anchor OAK uses to argue that NFT-collateral lending is its own Technique rather than a lending-protocol footnote. Read as a generic bug, the finding is "missing state check" and the lesson evaporates into a truism. Read as an NFT case, the specific hazard becomes legible: the collateral is a named, indivisible item, its custody is a single canonical on-chain fact, and the protocol chose to track that fact in its own mutable records instead of reading it. Fungible lending does not present that choice — a balance is the record. Non-fungibility introduces a second source of truth, and this incident is what it costs to trust the wrong one.

The recovery deserves a sober reading rather than a warm one. XCarnival got roughly half back inside a day by naming the address, offering 1,500 ETH, and waiving legal action, and it advertised a claim about law-enforcement geolocation while doing so. That combination is a negotiation, and it worked; it is also unrepeatable as a control. The protocol had no mechanism that limited the loss, so its outcome depended entirely on an attacker's choice. Contributors should record such returns as attacker-side dispositions — facts about one operator's decision — and never as evidence that the design was resilient. The one durable, reusable observation from the aftermath is that NFT collateral is hard to launder: a Bored Ape is a named object with a public provenance trail, which materially changes the economics of returning versus keeping, and helps explain why this sector's recovery rate is out of line with DeFi's generally.

Finally, the pairing with Omni Protocol two weeks later is the reason both entries exist. Same sector, same month, same underlying question — can the protocol prove it still holds the token? — and two entirely unrelated bugs producing the same answer: no. XCarnival lost custody-belief to a stale record; Omni lost it to a callback in the middle of a state transition. A defender enumerating an NFT-lending codebase should ask that single question of every path, because the two documented ways of failing it have nothing in common except the consequence.

Techniques demonstrated (3)